From 22f6692971c7a5912bc996730042643a3e32980f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 16:38:01 +0100 Subject: [PATCH 01/70] Port ESMC + ESMFold2 model code from fork onto v5 main (baseline, unadapted) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the purely-additive model code from the Biohub fork (github.com/Biohub/transformers @ f9a5a374b, based on v4.57.6) onto a branch off current main (v5.10.0.dev0). This is the verbatim fork code as a starting point; v5 convention adaptation (attention interface, modular, __all__, nested-config round-trip) is follow-up work per the port plan. Contents: - src/transformers/models/esmc/ (6 files: config, sae config, modeling, sae modeling, tokenizer) — imports and exports cleanly under v5. - src/transformers/models/esmfold2/ (24 files incl. deferred kernels/, distributed/, experimental) — config + modeling import; ESMFold2Model is defined but not yet exported (no __all__ — known adaptation item). Auto-registration adapted to the v5 layout (NOT a verbatim copy of the fork's 4 hook diffs): - models/__init__.py: from .esmc/.esmfold2 import * - auto/auto_mappings.py: CONFIG_MAPPING_NAMES + SPECIAL_MODEL_TYPE_TO_MODULE_NAME (these moved out of configuration_auto.py in v5; MODEL_NAMES_MAPPING was dropped in v5 so the fork's hunk for it has no equivalent). - auto/modeling_auto.py: base + masked-LM + seq-cls + token-cls maps. - auto/tokenization_auto.py: flat ("esmc", "ESMCTokenizer") form (v5 dropped the (slow, fast) tuple format). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/__init__.py | 2 + src/transformers/models/auto/auto_mappings.py | 5 + src/transformers/models/auto/modeling_auto.py | 6 + .../models/auto/tokenization_auto.py | 1 + src/transformers/models/esmc/__init__.py | 31 + .../models/esmc/configuration_esmc.py | 89 + .../models/esmc/configuration_esmc_sae.py | 77 + src/transformers/models/esmc/modeling_esmc.py | 1660 ++++++++++ .../models/esmc/modeling_esmc_sae.py | 363 +++ .../models/esmc/tokenization_esmc.py | 217 ++ src/transformers/models/esmfold2/__init__.py | 29 + .../models/esmfold2/configuration_esmfold2.py | 298 ++ .../models/esmfold2/distributed/__init__.py | 40 + .../models/esmfold2/distributed/comm.py | 384 +++ .../models/esmfold2/distributed/manager.py | 576 ++++ .../esmfold2/distributed/model/__init__.py | 2 + .../distributed/model/layers/__init__.py | 2 + .../distributed/model/layers/layernorm.py | 205 ++ .../distributed/model/layers/linear.py | 199 ++ .../distributed/model/layers/pairformer.py | 181 ++ .../model/layers/triangular_mult.py | 427 +++ .../models/esmfold2/distributed/utils.py | 483 +++ .../models/esmfold2/kernels/__init__.py | 21 + .../kernels/fused_attention_pair_bias.py | 697 +++++ .../kernels/fused_dropout_residual.py | 247 ++ .../esmfold2/kernels/fused_dual_gemm.py | 610 ++++ .../esmfold2/kernels/fused_ln_residual.py | 397 +++ .../esmfold2/kernels/fused_lnlin_swiglu.py | 415 +++ .../esmfold2/kernels/trimul_einsum_triton.py | 242 ++ .../esmfold2/kernels/trimul_with_residual.py | 622 ++++ .../models/esmfold2/modeling_esmfold2.py | 1163 +++++++ .../esmfold2/modeling_esmfold2_common.py | 2758 +++++++++++++++++ .../modeling_esmfold2_experimental.py | 1059 +++++++ .../models/esmfold2/protein_utils.py | 582 ++++ 34 files changed, 14090 insertions(+) create mode 100644 src/transformers/models/esmc/__init__.py create mode 100644 src/transformers/models/esmc/configuration_esmc.py create mode 100644 src/transformers/models/esmc/configuration_esmc_sae.py create mode 100644 src/transformers/models/esmc/modeling_esmc.py create mode 100644 src/transformers/models/esmc/modeling_esmc_sae.py create mode 100644 src/transformers/models/esmc/tokenization_esmc.py create mode 100644 src/transformers/models/esmfold2/__init__.py create mode 100644 src/transformers/models/esmfold2/configuration_esmfold2.py create mode 100644 src/transformers/models/esmfold2/distributed/__init__.py create mode 100644 src/transformers/models/esmfold2/distributed/comm.py create mode 100644 src/transformers/models/esmfold2/distributed/manager.py create mode 100644 src/transformers/models/esmfold2/distributed/model/__init__.py create mode 100644 src/transformers/models/esmfold2/distributed/model/layers/__init__.py create mode 100644 src/transformers/models/esmfold2/distributed/model/layers/layernorm.py create mode 100644 src/transformers/models/esmfold2/distributed/model/layers/linear.py create mode 100644 src/transformers/models/esmfold2/distributed/model/layers/pairformer.py create mode 100644 src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py create mode 100644 src/transformers/models/esmfold2/distributed/utils.py create mode 100644 src/transformers/models/esmfold2/kernels/__init__.py create mode 100644 src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py create mode 100644 src/transformers/models/esmfold2/kernels/fused_dropout_residual.py create mode 100644 src/transformers/models/esmfold2/kernels/fused_dual_gemm.py create mode 100644 src/transformers/models/esmfold2/kernels/fused_ln_residual.py create mode 100644 src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py create mode 100644 src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py create mode 100644 src/transformers/models/esmfold2/kernels/trimul_with_residual.py create mode 100644 src/transformers/models/esmfold2/modeling_esmfold2.py create mode 100644 src/transformers/models/esmfold2/modeling_esmfold2_common.py create mode 100644 src/transformers/models/esmfold2/modeling_esmfold2_experimental.py create mode 100644 src/transformers/models/esmfold2/protein_utils.py diff --git a/src/transformers/models/__init__.py b/src/transformers/models/__init__.py index 4660154353f6..d4c820c0ed64 100644 --- a/src/transformers/models/__init__.py +++ b/src/transformers/models/__init__.py @@ -138,6 +138,8 @@ from .ernie4_5_moe import * from .ernie4_5_vl_moe import * from .esm import * + from .esmc import * + from .esmfold2 import * from .evolla import * from .exaone4 import * from .exaone4_5 import * diff --git a/src/transformers/models/auto/auto_mappings.py b/src/transformers/models/auto/auto_mappings.py index 4bb04f98251b..905ebb97a884 100644 --- a/src/transformers/models/auto/auto_mappings.py +++ b/src/transformers/models/auto/auto_mappings.py @@ -176,6 +176,9 @@ ("ernie4_5_vl_moe_text", "Ernie4_5_VLMoeTextConfig"), ("ernie4_5_vl_moe_vision", "Ernie4_5_VLMoeVisionConfig"), ("esm", "EsmConfig"), + ("esmc", "ESMCConfig"), + ("esmc_sae", "ESMCSAEConfig"), + ("esmfold2", "ESMFold2Config"), ("eurobert", "EuroBertConfig"), ("evolla", "EvollaConfig"), ("exaone4", "Exaone4Config"), @@ -741,6 +744,8 @@ ("encoder-decoder", "encoder_decoder"), ("ernie4_5_vl_moe_text", "ernie4_5_vl_moe"), ("ernie4_5_vl_moe_vision", "ernie4_5_vl_moe"), + ("esmc_sae", "esmc"), + ("esmfold2_v2", "esmfold2"), ("exaone4_5_vision", "exaone4_5"), ("fastspeech2_conformer_hifigan", "fastspeech2_conformer"), ("fastspeech2_conformer_with_hifigan", "fastspeech2_conformer"), diff --git a/src/transformers/models/auto/modeling_auto.py b/src/transformers/models/auto/modeling_auto.py index 9b0d07d894a1..625351c1bd85 100644 --- a/src/transformers/models/auto/modeling_auto.py +++ b/src/transformers/models/auto/modeling_auto.py @@ -152,6 +152,9 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin): ("ernie4_5_moe", "Ernie4_5_MoeModel"), ("ernie4_5_vl_moe", "Ernie4_5_VLMoeModel"), ("esm", "EsmModel"), + ("esmc", "ESMCModel"), + ("esmc_sae", "ESMCSAEModel"), + ("esmfold2", "ESMFold2Model"), ("eurobert", "EuroBertModel"), ("evolla", "EvollaModel"), ("exaone4", "Exaone4Model"), @@ -1145,6 +1148,7 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin): ("electra", "ElectraForMaskedLM"), ("ernie", "ErnieForMaskedLM"), ("esm", "EsmForMaskedLM"), + ("esmc", "ESMCForMaskedLM"), ("eurobert", "EuroBertForMaskedLM"), ("flaubert", "FlaubertWithLMHeadModel"), ("fnet", "FNetForMaskedLM"), @@ -1339,6 +1343,7 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin): ("electra", "ElectraForSequenceClassification"), ("ernie", "ErnieForSequenceClassification"), ("esm", "EsmForSequenceClassification"), + ("esmc", "ESMCForSequenceClassification"), ("eurobert", "EuroBertForSequenceClassification"), ("exaone4", "Exaone4ForSequenceClassification"), ("falcon", "FalconForSequenceClassification"), @@ -1568,6 +1573,7 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin): ("electra", "ElectraForTokenClassification"), ("ernie", "ErnieForTokenClassification"), ("esm", "EsmForTokenClassification"), + ("esmc", "ESMCForTokenClassification"), ("eurobert", "EuroBertForTokenClassification"), ("exaone4", "Exaone4ForTokenClassification"), ("falcon", "FalconForTokenClassification"), diff --git a/src/transformers/models/auto/tokenization_auto.py b/src/transformers/models/auto/tokenization_auto.py index 222b2d55e1c4..5c83999350c2 100644 --- a/src/transformers/models/auto/tokenization_auto.py +++ b/src/transformers/models/auto/tokenization_auto.py @@ -119,6 +119,7 @@ ("emu3", "GPT2Tokenizer" if is_tokenizers_available() else None), ("ernie", "BertTokenizer" if is_tokenizers_available() else None), ("esm", "EsmTokenizer"), + ("esmc", "ESMCTokenizer"), ("falcon_mamba", "GPTNeoXTokenizer" if is_tokenizers_available() else None), ("fastspeech2_conformer", "FastSpeech2ConformerTokenizer" if is_g2p_en_available() else None), ("flaubert", "FlaubertTokenizer"), diff --git a/src/transformers/models/esmc/__init__.py b/src/transformers/models/esmc/__init__.py new file mode 100644 index 000000000000..07d913a15255 --- /dev/null +++ b/src/transformers/models/esmc/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TYPE_CHECKING + +from ...utils import _LazyModule # type: ignore[import] +from ...utils.import_utils import define_import_structure # type: ignore[import] + +if TYPE_CHECKING: + from .configuration_esmc import * # noqa: F403 + from .configuration_esmc_sae import * # noqa: F403 + from .modeling_esmc import * # noqa: F403 + from .modeling_esmc_sae import * # noqa: F403 + from .tokenization_esmc import * # noqa: F403 +else: + import sys + + _file = globals()["__file__"] + sys.modules[__name__] = _LazyModule( + __name__, _file, define_import_structure(_file), module_spec=__spec__ + ) diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py new file mode 100644 index 000000000000..962a23e46163 --- /dev/null +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -0,0 +1,89 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""ESMC model configuration.""" + +from ...configuration_utils import PretrainedConfig # type: ignore[import] + + +class ESMCConfig(PretrainedConfig): + """ + This is the configuration class to store the configuration of a [`ESMCModel`]. It is used to + instantiate an ESMC model according to the specified arguments, defining the model architecture. + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model + outputs. Read the documentation from [`PretrainedConfig`] for more information. + + Args: + vocab_size (`int`, *optional*, defaults to 64): + Vocabulary size of the ESMC model. Defines the number of different amino acid tokens that + can be represented by the ``input_ids`` passed to [`ESMCModel`]. + d_model (`int`, *optional*, defaults to 2560): + Dimensionality of the encoder layers and the pooler layer. + n_heads (`int`, *optional*, defaults to 40): + Number of attention heads for each attention layer in the Transformer encoder. + n_layers (`int`, *optional*, defaults to 80): + Number of hidden layers in the Transformer encoder. + pad_token_id (`int`, *optional*, defaults to 1): + Index of the padding token in the vocabulary (``""``). + mask_token_id (`int`, *optional*, defaults to 32): + Index of the mask token in the vocabulary (``""``), used for masked language modelling. + initializer_range (`float`, *optional*, defaults to 0.02): + The standard deviation of the truncated normal initialiser for weight matrix initialisation. + classifier_dropout (`float`, *optional*, defaults to 0.1): + Dropout ratio for the classification head. + + Examples: + + ```python + >>> from transformers import ESMCConfig, ESMCModel + + >>> # Initializing an ESMC EvolutionaryScale/esmc-600m-2024-12 style configuration + >>> configuration = ESMCConfig() + + >>> # Initializing a model (with random weights) from the EvolutionaryScale/esmc-600m-2024-12 style configuration + >>> model = ESMCModel(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ``` + """ + + model_type = "esmc" + + def __init__( + self, + vocab_size: int = 64, + d_model: int = 2560, + n_heads: int = 40, + n_layers: int = 80, + pad_token_id: int = 1, + mask_token_id: int = 32, + initializer_range: float = 0.02, + classifier_dropout: float = 0.1, + **kwargs, + ): + super().__init__( + pad_token_id=pad_token_id, mask_token_id=mask_token_id, **kwargs + ) + + self.vocab_size = vocab_size + self.d_model = d_model + self.n_heads = n_heads + self.n_layers = n_layers + self.initializer_range = initializer_range + self.classifier_dropout = classifier_dropout + self.tie_word_embeddings = False + + +__all__ = ["ESMCConfig"] diff --git a/src/transformers/models/esmc/configuration_esmc_sae.py b/src/transformers/models/esmc/configuration_esmc_sae.py new file mode 100644 index 000000000000..42ecc1f32c09 --- /dev/null +++ b/src/transformers/models/esmc/configuration_esmc_sae.py @@ -0,0 +1,77 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""ESMC sparse autoencoder (SAE) configuration.""" + +from dataclasses import dataclass + +from ...configuration_utils import PretrainedConfig # type: ignore[import] + + +@dataclass +class ESMCSAEParams: + """Parameters for one backbone layer's SAE inside :class:`ESMCSAEModel`. + + The SAE itself is an internal ``nn.Module``; this dataclass just bundles + the handful of fields needed to instantiate one. + """ + + d_model: int = 2560 + codebook_dim: int = 65536 + k: int = 64 + layer: int = 0 + + +class ESMCSAEConfig(PretrainedConfig): + """ + Configuration class for [`ESMCSAEModel`] — a container that holds one + SAE per backbone layer for a fixed ``(model, codebook_dim, k)`` group. + + All SAEs in a container share ``d_model``, ``codebook_dim``, and ``k``; + they differ only in the backbone layer they were trained on. + ``available_layers`` lists the backbone-layer indices the repo ships; + each entry ``i`` is stored on disk as ``layer_{i}.safetensors`` (the + filename index *is* the backbone layer, so a single-layer repo for + layer 23 stores ``layer_23.safetensors``). + + Args: + d_model (`int`, *optional*, defaults to 2560): + Dimensionality of the ESMC hidden states fed into the SAEs. + codebook_dim (`int`, *optional*, defaults to 65536): + Number of sparse features in each SAE's codebook. + k (`int`, *optional*, defaults to 64): + Top-k sparsity per SAE. + available_layers (`list[int]`, *optional*, defaults to ``[0]``): + Which backbone-layer indices the repo ships. + """ + + model_type = "esmc_sae" + + def __init__( + self, + d_model: int = 2560, + codebook_dim: int = 65536, + k: int = 64, + available_layers: list[int] | None = None, + **kwargs, + ): + super().__init__(**kwargs) + self.d_model = d_model + self.codebook_dim = codebook_dim + self.k = k + self.available_layers = ( + list(available_layers) if available_layers is not None else [0] + ) + + +__all__ = ["ESMCSAEConfig", "ESMCSAEParams"] diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py new file mode 100644 index 000000000000..f48a6ac51706 --- /dev/null +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -0,0 +1,1660 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""PyTorch ESMC model.""" + +import math +import re +from dataclasses import dataclass +from typing import Optional, cast + +import torch +import torch.nn as nn +from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss +from torch.nn import functional as F + +from ...modeling_outputs import ( # type: ignore[import] + MaskedLMOutput, + ModelOutput, + SequenceClassifierOutput, + TokenClassifierOutput, +) +from ...modeling_utils import PreTrainedModel # type: ignore[import] +from ...utils import ( # type: ignore[import] + auto_docstring, + can_return_tuple, + is_flash_attn_2_available, + logging, +) +from .configuration_esmc import ESMCConfig +from .modeling_esmc_sae import _ESMCSAELayer + +logger = logging.get_logger(__name__) + +_CONFIG_FOR_DOC = "ESMCConfig" + +# Optional accelerated kernels. Pure-PyTorch fallbacks below if absent. +if is_flash_attn_2_available(): + from flash_attn import flash_attn_varlen_qkvpacked_func + from flash_attn.bert_padding import pad_input, unpad_input + + _flash_attn_available = True +else: + pad_input = unpad_input = flash_attn_varlen_qkvpacked_func = None + _flash_attn_available = False + +try: + from flash_attn.ops.triton.rotary import apply_rotary as apply_triton_rotary + + _flash_attn_rotary_available = torch.cuda.is_available() +except ImportError: + apply_triton_rotary = None # type: ignore[assignment] + _flash_attn_rotary_available = False + +# Transformer Engine: fused LayerNorm+Linear / LayerNorm+MLP kernels with +# fp32 reduction inside the LayerNorm. Recommended on GPU for accurate bf16 +# inference; without it the pure-PyTorch fallback drifts ~O(10) in fp32 and +# ~O(100) in bf16 on the unnormalized residual stream (perplexity stays +# within rounding noise). +try: + import transformer_engine.pytorch as te # type: ignore[import-untyped] + + _te_available = True +except ImportError: + te = None # type: ignore[assignment] + _te_available = False + +# xformers: preferred SDPA implementation on GPU. Provides a fused +# bf16 attention kernel with deterministic reduction order. Flash +# Attention 2 and PyTorch's ``F.scaled_dot_product_attention`` are +# progressively-less-preferred fallbacks. +try: + import xformers.ops as xops # type: ignore[import-untyped] + + _xformers_available = True +except ImportError: + xops = None # type: ignore[assignment] + _xformers_available = False + +# Flash Attention 2: secondary SDPA fallback. Used when xformers is not +# installed; fp16 / bf16 only. +if _flash_attn_available: + from flash_attn import flash_attn_func +else: + flash_attn_func = None # type: ignore[assignment] + +if not _te_available: + logger.warning( + "ESMC: transformer_engine is not installed; falling back to " + "pure-PyTorch LayerNorm+Linear / LayerNorm+MLP. Outputs will differ " + "numerically — measured on the unnormalized residual stream (before " + "the final LayerNorm), ~O(10) max-diff in fp32 and ~O(100) in bf16; " + "after the final LayerNorm these shrink to a few ULP and perplexity " + "stays within rounding noise. Install with " + "`pip install transformer-engine[pytorch]` to enable fused fp32-" + "reduction LayerNorm." + ) + +if not _xformers_available and not _flash_attn_available: + logger.warning( + "ESMC: neither xformers nor flash-attn is installed; falling back " + "to PyTorch ``F.scaled_dot_product_attention``. The attention " + "reduction order in bf16 differs from a fused kernel by ~1 bf16 " + "ULP per attention block; compounded across the 80-block stack " + "this reaches ~O(100) max-diff on the unnormalized residual stream. " + "Install xformers (preferred) with `pip install xformers` for a " + "fused attention kernel." + ) + +if torch.cuda.is_available() and not _flash_attn_rotary_available: + logger.warning( + "ESMC: flash-attn rotary kernel not installed; falling back to " + "pure-PyTorch RoPE. For faster GPU inference run `pip install flash-attn`." + ) + + +# --------------------------------------------------------------------------- +# Output dataclasses +# --------------------------------------------------------------------------- + + +@dataclass +class ESMCOutput(ModelOutput): + """ + Args: + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Sequence of hidden states at the output of the last layer, after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states for all encoder layers. + Shape ``(n_layers, batch_size, sequence_length, d_model)``. + Returned when ``output_hidden_states=True``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + Only populated when SAE models have been registered via + ``add_sae_models`` and ``compute_sae=True``. + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. Not available on the + ``flash_attention_2`` path. + """ + + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +@dataclass +class ESMCMaskedLMOutput(MaskedLMOutput): + """ + Args: + loss (`torch.FloatTensor` of shape `(1,)`, *optional*): + Masked language modelling loss. Returned when ``labels`` are provided. + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, vocab_size)`): + Prediction scores of the language modelling head. + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Final hidden states after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. + """ + + loss: torch.FloatTensor | None = None + logits: torch.FloatTensor | None = None + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +@dataclass +class ESMCTokenClassifierOutput(TokenClassifierOutput): + """ + Args: + loss (`torch.FloatTensor` of shape `(1,)`, *optional*): + Token classification loss. Returned when ``labels`` are provided. + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, num_labels)`): + Classification scores (before SoftMax). + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Final hidden states after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. + """ + + loss: torch.FloatTensor | None = None + logits: torch.FloatTensor | None = None + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +@dataclass +class ESMCSequenceClassifierOutput(SequenceClassifierOutput): + """ + Args: + loss (`torch.FloatTensor` of shape `(1,)`, *optional*): + Sequence classification loss. Returned when ``labels`` are provided. + logits (`torch.FloatTensor` of shape `(batch_size, num_labels)`): + Classification scores (before SoftMax). + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Final hidden states after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. + """ + + loss: torch.FloatTensor | None = None + logits: torch.FloatTensor | None = None + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +# --------------------------------------------------------------------------- +# Rotary position embedding helpers +# --------------------------------------------------------------------------- + + +def _rotate_half(x: torch.Tensor, interleaved: bool = False) -> torch.Tensor: + if not interleaved: + x1, x2 = x.chunk(2, dim=-1) + return torch.cat((-x2, x1), dim=-1) + x1, x2 = x[..., ::2], x[..., 1::2] + return torch.stack((-x2, x1), dim=-1).flatten(-2, -1) + + +def _apply_rotary_emb_torch( + x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, interleaved: bool = False +) -> torch.Tensor: + """Apply rotary position embeddings (pure PyTorch, no Triton dependency). + + Args: + x: ``(batch, seqlen, n_heads, head_dim)`` + cos: ``(seqlen, rotary_dim / 2)`` + sin: ``(seqlen, rotary_dim / 2)`` + """ + ro_dim = cos.shape[-1] * 2 + seqlen = x.size(1) + cos = cos[:seqlen].unsqueeze(1).repeat(1, 1, 2) + sin = sin[:seqlen].unsqueeze(1).repeat(1, 1, 2) + return torch.cat( + [ + x[..., :ro_dim] * cos + _rotate_half(x[..., :ro_dim], interleaved) * sin, + x[..., ro_dim:], + ], + dim=-1, + ) + + +class RotaryEmbedding(nn.Module): + """Rotary position embeddings (RoPE) as described in `RoFormer`_. + + .. _RoFormer: https://arxiv.org/abs/2104.09864 + + Args: + dim: Size of a single attention head. + base: Frequency base for the sinusoidal positions. + interleaved: If ``True`` rotate adjacent pairs (GPT-J style) instead of + splitting the head dimension in half (GPT-NeoX style). + scaling_factor: Linear scaling factor applied to position indices. + pos_idx_in_fp32: Compute position indices in float32 to avoid bf16 + rounding errors at large sequence lengths. + """ + + def __init__( + self, + dim: int, + base: float = 10000.0, + interleaved: bool = False, + scale_base: float | None = None, + scaling_factor: float = 1.0, + pos_idx_in_fp32: bool = True, + device=None, + ): + super().__init__() + self.dim = dim + self.base = base + self.interleaved = interleaved + self.scale_base = scale_base + self.scaling_factor = scaling_factor + self.pos_idx_in_fp32 = pos_idx_in_fp32 + + self._seq_len_cached = 0 + self._cos_cached: torch.Tensor | None = None + self._sin_cached: torch.Tensor | None = None + self._cos_k_cached: torch.Tensor | None = None + self._sin_k_cached: torch.Tensor | None = None + + self.reset_parameters(device=device) + + def reset_parameters(self, device=None): + inv_freq = self._compute_inv_freq(device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + arange = torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) + scale = ( + (arange + 0.4 * self.dim) / (1.4 * self.dim) + if self.scale_base is not None + else None + ) + self.register_buffer("scale", scale, persistent=False) + + def _compute_inv_freq(self, device=None) -> torch.Tensor: + return 1.0 / ( + self.base + ** ( + torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) + / self.dim + ) + ) + + def _update_cos_sin_cache(self, seqlen: int, device=None, dtype=None): + if self.inv_freq.is_meta: + self.reset_parameters(device=device) + if ( + seqlen > self._seq_len_cached + or self._cos_cached is None + or self._cos_cached.device != device + or self._cos_cached.dtype != dtype + or (self.training and self._cos_cached.is_inference()) + ): + self._seq_len_cached = seqlen + if self.pos_idx_in_fp32: + t = ( + torch.arange(seqlen, device=device, dtype=torch.float32) + / self.scaling_factor + ) + inv_freq = ( + self.inv_freq.to(torch.float32) + if self.inv_freq.dtype != torch.float32 + else self.inv_freq + ) + else: + t = ( + torch.arange(seqlen, device=device, dtype=self.inv_freq.dtype) # type: ignore[call-overload] + / self.scaling_factor + ) + inv_freq = self.inv_freq + freqs = torch.outer(t, inv_freq) # type: ignore[arg-type] + + if self.scale is None: + self._cos_cached = torch.cos(freqs).to(dtype) + self._sin_cached = torch.sin(freqs).to(dtype) + else: + _scale: torch.Tensor = self.scale # type: ignore[assignment] + power = ( + torch.arange(seqlen, dtype=_scale.dtype, device=_scale.device) + - seqlen // 2 + ) / self.scale_base # type: ignore[operator] + scale = _scale.to(device=power.device) ** power.unsqueeze(-1) + self._cos_cached = (torch.cos(freqs) * scale).to(dtype) + self._sin_cached = (torch.sin(freqs) * scale).to(dtype) + self._cos_k_cached = (torch.cos(freqs) / scale).to(dtype) + self._sin_k_cached = (torch.sin(freqs) / scale).to(dtype) + + def _apply(self, fn, recurse=True): + if self.inv_freq.is_meta: + self.reset_parameters(device="cpu") + result = super()._apply(fn, recurse=recurse) + # Recompute inv_freq on the new device: CPU vs CUDA ``pow`` differ by + # ~1 fp32 ULP, which compounds across attention layers. Keep this + # buffer fp32 even when the module is cast to bf16/fp16; otherwise the + # rounded RoPE frequencies drift from the internal ESMC path. + new_inv_freq = self._compute_inv_freq(device=self.inv_freq.device) + self.register_buffer("inv_freq", new_inv_freq, persistent=False) + self._seq_len_cached = 0 + self._cos_cached = None + self._sin_cached = None + self._cos_k_cached = None + self._sin_k_cached = None + return result + + def forward( + self, q: torch.Tensor, k: torch.Tensor, seqlen_offset: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Apply RoPE to query and key tensors. + + Args: + q: ``(batch, seqlen, n_heads, head_dim)`` + k: ``(batch, seqlen, n_heads, head_dim)`` + seqlen_offset: Offset used in incremental decoding. + + Returns: + Tuple of rotated ``(q, k)`` tensors with the same shape as the inputs. + """ + self._update_cos_sin_cache( + q.shape[1] + seqlen_offset, device=q.device, dtype=q.dtype + ) + assert self._cos_cached is not None and self._sin_cached is not None + + if self.scale is not None: + raise NotImplementedError("XPos scaling is not supported in this path.") + + cos = self._cos_cached[seqlen_offset:] + sin = self._sin_cached[seqlen_offset:] + + if _flash_attn_rotary_available and q.device.type == "cuda": + q_rot = apply_triton_rotary(q, cos, sin, interleaved=self.interleaved) # type: ignore[misc] + k_rot = apply_triton_rotary(k, cos, sin, interleaved=self.interleaved) # type: ignore[misc] + else: + q_rot = _apply_rotary_emb_torch(q, cos, sin, self.interleaved) + k_rot = _apply_rotary_emb_torch(k, cos, sin, self.interleaved) + return q_rot, k_rot + + +class _TritonRotaryEmbedding(RotaryEmbedding): + """RoPE variant that delegates to the Flash-Attention Triton kernel. + + Only used inside :class:`_FlashMultiHeadAttention` when Flash Attention 2 + is available. The ``forward`` signature differs from :class:`RotaryEmbedding` + because Flash Attention packs Q, K, V together. + """ + + def forward( + self, qkv: torch.Tensor, cu_seqlens: torch.Tensor, max_seqlen: int + ) -> torch.Tensor: # type: ignore[override] + """Apply RoPE in-place to a packed ``(N, 3, n_heads, head_dim)`` tensor.""" + self._update_cos_sin_cache(max_seqlen, device=qkv.device, dtype=qkv.dtype) + assert self._cos_cached is not None and self._sin_cached is not None + assert apply_triton_rotary is not None + + apply_triton_rotary( + qkv[:, 0], + self._cos_cached, + self._sin_cached, + cu_seqlens=cu_seqlens, + max_seqlen=max_seqlen, + inplace=True, + ) + apply_triton_rotary( + qkv[:, 1], + self._cos_cached, + self._sin_cached, + cu_seqlens=cu_seqlens, + max_seqlen=max_seqlen, + inplace=True, + ) + return qkv + + +# --------------------------------------------------------------------------- +# Feed-forward network helpers +# --------------------------------------------------------------------------- + + +def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: + """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" + return int(((expansion_ratio * d_model) + 255) // 256 * 256) + + +class _SwiGLU(nn.Module): + """SwiGLU activation: ``silu(x1) * x2`` where ``x`` is split along the last dim.""" + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x1, x2 = x.chunk(2, dim=-1) + return F.silu(x1) * x2 + + +class _PyTorchLayerNormLinear(nn.Module): + """LayerNorm followed by a Linear projection, sharing the parameter + names ``layer_norm_weight``, ``layer_norm_bias`` and ``weight`` so the + state-dict layout matches the accelerated TE module loaded on GPU. + """ + + def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: + super().__init__() + self.d_in = d_in + self.eps = eps + self.layer_norm_weight = nn.Parameter(torch.ones(d_in)) + self.layer_norm_bias = nn.Parameter(torch.zeros(d_in)) + self.weight = nn.Parameter(torch.empty(d_out, d_in)) + nn.init.normal_(self.weight, std=0.02) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = F.layer_norm( + x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps + ) + return F.linear(x, self.weight) + + +class _PyTorchLayerNormMLP(nn.Module): + """LayerNorm + SwiGLU MLP, sharing the parameter names + ``layer_norm_weight``, ``layer_norm_bias``, ``fc1_weight``, + ``fc2_weight`` so the state-dict layout matches the accelerated TE + module loaded on GPU. + """ + + def __init__( + self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5 + ) -> None: + super().__init__() + self.hidden_size = hidden_size + self.ffn_hidden_size = ffn_hidden_size + self.eps = eps + self.layer_norm_weight = nn.Parameter(torch.ones(hidden_size)) + self.layer_norm_bias = nn.Parameter(torch.zeros(hidden_size)) + self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, hidden_size)) + self.fc2_weight = nn.Parameter(torch.empty(hidden_size, ffn_hidden_size)) + nn.init.normal_(self.fc1_weight, std=0.02) + nn.init.normal_(self.fc2_weight, std=0.02) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = F.layer_norm( + x, + (self.hidden_size,), + self.layer_norm_weight, + self.layer_norm_bias, + self.eps, + ) + x = F.linear(x, self.fc1_weight) + x1, x2 = x.chunk(2, dim=-1) + x = F.silu(x1) * x2 + return F.linear(x, self.fc2_weight) + + +def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: + """LayerNorm + SwiGLU MLP. Uses Transformer Engine's fused LN+MLP when + available; otherwise returns the pure-PyTorch fallback with matching + state-dict layout.""" + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + hidden = _swiglu_hidden_dim(expansion_ratio, d_model) + if _te_available: + return te.LayerNormMLP( # type: ignore[union-attr] + hidden_size=d_model, + ffn_hidden_size=hidden, + bias=bias, + activation="swiglu", + init_method=None, + output_layer_init_method=None, + ) + return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) + + +def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: + """LayerNorm + fused QKV projection. Uses Transformer Engine when + available; pure-PyTorch fallback otherwise.""" + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + if _te_available: + return te.LayerNormLinear( # type: ignore[union-attr] + d_model, d_model * 3, bias=bias, init_method=None + ) + return _PyTorchLayerNormLinear(d_model, d_model * 3) + + +def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: + """Attention output projection. Uses Transformer Engine when available; + pure-PyTorch ``nn.Linear`` otherwise.""" + if _te_available: + return te.Linear( # type: ignore[union-attr] + d_model, d_model, bias=bias, init_method=None + ) + return nn.Linear(d_model, d_model, bias=bias) + + +def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequential: + hidden = int(expansion_ratio * d_model) + return nn.Sequential( + nn.LayerNorm(d_model), + nn.Linear(d_model, hidden, bias=bias), + nn.GELU(), + nn.Linear(hidden, d_model, bias=bias), + ) + + +# --------------------------------------------------------------------------- +# Attention +# --------------------------------------------------------------------------- + + +def _scaled_dot_product_attention( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + n_heads: int, + d_head: int, + seq_id: torch.Tensor | None, +) -> torch.Tensor: + """Scaled dot-product attention with optional chain-aware mask. + + Dispatches in order of preference: + 1. xformers ``memory_efficient_attention`` — preferred fused kernel, + requires ``xformers``, no chain mask. + 2. Flash Attention 2 (``flash_attn.flash_attn_func``) — secondary + fused kernel, requires ``flash-attn``, no chain mask, fp16 / + bf16 only. + 3. PyTorch's ``F.scaled_dot_product_attention`` — last-resort path; + also handles the chain-aware mask when ``seq_id`` is present + and the fp32 path that Flash Attention 2 does not support. + """ + if seq_id is None and _xformers_available: + b, s, _ = q.shape + q4 = q.view(b, s, n_heads, d_head) + k4 = k.view(b, s, n_heads, d_head) + v4 = v.view(b, s, n_heads, d_head) + context = xops.memory_efficient_attention( # type: ignore[union-attr] + q4, k4, v4, attn_bias=None, scale=d_head**-0.5 + ) + return context.reshape(b, s, n_heads * d_head) + if ( + seq_id is None + and _flash_attn_available + and q.dtype in (torch.float16, torch.bfloat16) + ): + b, s, _ = q.shape + q4 = q.view(b, s, n_heads, d_head) + k4 = k.view(b, s, n_heads, d_head) + v4 = v.view(b, s, n_heads, d_head) + context = flash_attn_func( # type: ignore[misc] + q4, k4, v4, dropout_p=0.0, softmax_scale=d_head**-0.5 + ) + return context.reshape(b, s, n_heads * d_head) # type: ignore[union-attr] + b, s, _ = q.shape + q = q.view(b, s, n_heads, -1).transpose(1, 2) + k = k.view(b, s, n_heads, -1).transpose(1, 2) + v = v.view(b, s, n_heads, -1).transpose(1, 2) + if seq_id is not None: + mask = (seq_id.unsqueeze(-1) == seq_id.unsqueeze(-2)).unsqueeze(1) + context = F.scaled_dot_product_attention(q, k, v, mask) + else: + context = F.scaled_dot_product_attention(q, k, v) + _, h, _, d_out = context.shape + return context.transpose(1, 2).reshape(b, s, h * d_out) + + +class MultiHeadAttention(nn.Module): + """Multi-head self-attention with QK LayerNorm and RoPE. + + Args: + d_model: Model hidden dimension. + n_heads: Number of attention heads. + bias: Whether to use bias in linear layers. + qk_layernorm: Whether to apply LayerNorm to queries and keys before + computing attention scores. + """ + + def __init__( + self, d_model: int, n_heads: int, bias: bool = False, qk_layernorm: bool = True + ): + super().__init__() + self.d_model = d_model + self.n_heads = n_heads + self.d_head = d_model // n_heads + + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) + self.out_proj = _make_attn_out_proj(d_model, bias) + + if qk_layernorm: + self.q_ln = nn.LayerNorm(d_model, bias=bias) + self.k_ln = nn.LayerNorm(d_model, bias=bias) + else: + self.q_ln = nn.Identity() + self.k_ln = nn.Identity() + + self.rotary = RotaryEmbedding(d_model // n_heads) + + def _apply_rotary( + self, q: torch.Tensor, k: torch.Tensor + ) -> tuple[torch.Tensor, torch.Tensor]: + q = q.unflatten(-1, (self.n_heads, self.d_head)) + k = k.unflatten(-1, (self.n_heads, self.d_head)) + q, k = self.rotary(q, k) + q = q.flatten(-2, -1) + k = k.flatten(-2, -1) + return q, k + + def forward( + self, + x: torch.Tensor, + seq_id: torch.Tensor | None, + output_attentions: bool = False, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + """Return ``(context, attn_weights)``. + + ``attn_weights`` is ``None`` unless ``output_attentions=True`` — the + fused SDPA backends (xformers, flash-attn 2, ``F.scaled_dot_product_attention``) + don't expose attention probabilities, so capturing them forces a + materialized ``softmax(Q @ K.T / sqrt(d)) @ V`` path with shape + ``(B, H, L, L)``. + """ + qkv = self.layernorm_qkv(x) + q, k, v = torch.chunk(qkv, 3, dim=-1) + q = self.q_ln(q).to(q.dtype) + k = self.k_ln(k).to(q.dtype) + q, k = self._apply_rotary(q, k) + + b, s, _ = q.shape + + if output_attentions: + # Manual SDPA so attention probabilities are observable. + q4 = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + k4 = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + v4 = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + scale = self.d_head**-0.5 + attn_scores = (q4 @ k4.transpose(-2, -1)) * scale + if seq_id is not None: + mask = (seq_id.unsqueeze(-1) == seq_id.unsqueeze(-2)).unsqueeze(1) + attn_scores = attn_scores.masked_fill(~mask, float("-inf")) + attn_weights = torch.softmax(attn_scores, dim=-1) + context = (attn_weights @ v4).transpose(1, 2).reshape(b, s, -1) + return self.out_proj(context), attn_weights + + context = _scaled_dot_product_attention( + q, k, v, n_heads=self.n_heads, d_head=self.d_head, seq_id=seq_id + ) + return self.out_proj(context), None + + +class _FlashMultiHeadAttention(MultiHeadAttention): + """Flash-Attention 2 variant of :class:`MultiHeadAttention`.""" + + def __init__( + self, d_model: int, n_heads: int, bias: bool = False, qk_layernorm: bool = True + ): + super().__init__( + d_model=d_model, n_heads=n_heads, bias=bias, qk_layernorm=qk_layernorm + ) + self.rotary = _TritonRotaryEmbedding(d_model // n_heads) + + def forward( + self, + x: torch.Tensor, + seq_id: torch.Tensor | None, + output_attentions: bool = False, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + if output_attentions: + raise ValueError( + "output_attentions=True is not supported with " + "attn_implementation='flash_attention_2'. " + "Re-load the model with attn_implementation='sdpa' (or 'eager')." + ) + assert seq_id is not None and seq_id.dtype == torch.bool + + seqlens = seq_id.sum(dim=-1, dtype=torch.int32) + cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) + max_seqlen = int(seqlens.max().item()) + + qkv = self.layernorm_qkv(x) + q, k, v = torch.chunk(qkv, 3, dim=-1) + q = self.q_ln(q).to(q.dtype) + k = self.k_ln(k).to(q.dtype) + + # ``q``/``k``/``v`` are 2D ``(T, D)`` here: the parent ``ESMCModel.forward`` + # calls ``unpad_input`` before the transformer stack to produce the + # varlen-flat layout that ``flash_attn_varlen_qkvpacked_func`` requires. + T = q.shape[0] + qkv_packed = torch.stack([q, k, v], dim=1).view(T, 3, self.n_heads, self.d_head) + qkv_packed = self.rotary(qkv_packed, cu_seqlens, max_seqlen) + + context = flash_attn_varlen_qkvpacked_func( # type: ignore[misc] + qkv_packed, cu_seqlens, max_seqlen, softmax_scale=self.d_head**-0.5 + ) + n_out, h_out, d_out = context.shape # type: ignore[union-attr] + return ( + self.out_proj(context.reshape(n_out, h_out * d_out)), # type: ignore[union-attr] + None, + ) + + +# --------------------------------------------------------------------------- +# Transformer blocks +# --------------------------------------------------------------------------- + + +class UnifiedTransformerBlock(nn.Module): + """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. + + Args: + d_model: Hidden dimension. + n_heads: Number of attention heads. + use_flash_attn: Use Flash Attention 2 kernel if available. + bias: Whether linear layers include bias terms. + expansion_ratio: Hidden-dim expansion ratio for the FFN. + residue_scaling_factor: Scales residual connections to stabilise deep + networks (``1 / sqrt(n_layers / 36)`` is the ESM3 scheme). + qk_layernorm: Whether to apply QK LayerNorm in attention. + ffn_type: Feed-forward activation: ``"swiglu"`` or ``"gelu"``. + """ + + def __init__( + self, + d_model: int, + n_heads: int, + use_flash_attn: bool = False, + bias: bool = False, + expansion_ratio: float = 4.0, + residue_scaling_factor: float = 1.0, + qk_layernorm: bool = True, + ffn_type: str = "swiglu", + ): + super().__init__() + + attn_cls = _FlashMultiHeadAttention if use_flash_attn else MultiHeadAttention + self.attn = attn_cls(d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + + if ffn_type == "swiglu": + self.ffn = _swiglu_ln_ffn(d_model, expansion_ratio, bias) + elif ffn_type == "gelu": + self.ffn = _gelu_ln_ffn(d_model, expansion_ratio, bias) + else: + raise ValueError( + f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'." + ) + + self.scaling_factor = residue_scaling_factor + + def forward( + self, + x: torch.Tensor, + sequence_id: torch.Tensor | None, + output_attentions: bool = False, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + """ + Args: + x: ``(batch, seq_len, d_model)`` + sequence_id: ``(batch, seq_len)`` chain-ID tensor used to restrict + attention to tokens within the same chain. SDPA blocks accept + an integer tensor (``-1`` marks padding); the flash-attn block + takes a ``bool`` padding mask — the caller selects which. + ``None`` skips chain-aware masking entirely (fast path). + output_attentions: When ``True``, returns the per-head attention + weights for this block alongside the residual output. + + Returns: + ``(output, attn_weights_or_None)``. Shape of ``output`` is + ``(batch, seq_len, d_model)``; ``attn_weights`` shape is + ``(batch, num_heads, seq_len, seq_len)`` or ``None``. + """ + attn_out, attn_weights = self.attn( + x, sequence_id, output_attentions=output_attentions + ) + x = x + attn_out / self.scaling_factor + x = x + self.ffn(x) / self.scaling_factor + return x, attn_weights + + +class TransformerStack(nn.Module): + """Stack of :class:`UnifiedTransformerBlock` layers with a final LayerNorm. + + Args: + d_model: Hidden dimension. + n_heads: Number of attention heads. + n_layers: Number of transformer blocks. + scale_residue: When ``True`` apply ESM3 residue scaling + ``sqrt(n_layers / 36)`` to each block. + bias: Bias flag forwarded to every sub-module. + qk_layernorm: QK LayerNorm flag forwarded to every block. + ffn_type: FFN activation type (``"swiglu"`` or ``"gelu"``). + expansion_ratio: FFN expansion ratio. + use_flash_attn: Use Flash Attention 2 kernel when available. + """ + + def __init__( + self, + d_model: int, + n_heads: int, + n_layers: int, + scale_residue: bool = True, + bias: bool = False, + qk_layernorm: bool = True, + ffn_type: str = "swiglu", + expansion_ratio: float = 8 / 3, + use_flash_attn: bool = False, + ): + super().__init__() + self.blocks = nn.ModuleList( + [ + UnifiedTransformerBlock( + d_model, + n_heads, + use_flash_attn=use_flash_attn, + residue_scaling_factor=math.sqrt(n_layers / 36) + if scale_residue + else 1.0, + expansion_ratio=expansion_ratio, + bias=bias, + qk_layernorm=qk_layernorm, + ffn_type=ffn_type, + ) + for _ in range(n_layers) + ] + ) + self.norm = nn.LayerNorm(d_model, bias=False) + + def forward( + self, + x: torch.Tensor, + sequence_id: torch.Tensor | None = None, + layers_to_collect: list[int] | None = None, + output_attentions: bool = False, + ) -> tuple[ + torch.Tensor, + torch.Tensor, + tuple[torch.Tensor, ...], + tuple[torch.Tensor, ...] | None, + ]: + """Run the full transformer stack. + + Args: + x: ``(batch, seq_len, d_model)`` + sequence_id: Optional chain-id tensor forwarded to each block. + layers_to_collect: Layer indices (0-based pre-block inputs plus + ``n_layers`` for the post-norm output) whose hidden states + should be returned. + output_attentions: When ``True``, collects the per-block attention + weights and returns them as the fourth tuple element. + + Returns: + ``(post_norm, pre_norm, hidden_states, attentions)`` where + ``hidden_states`` is a (possibly empty) tuple of tensors and + ``attentions`` is a tuple of per-block ``(B, H, L, L)`` tensors + or ``None`` when ``output_attentions`` is ``False``. + """ + if layers_to_collect is None: + layers_to_collect = [] + + collected: list[torch.Tensor] = [] + all_attentions: list[torch.Tensor] = [] + for layer_idx, block in enumerate(self.blocks): + if layer_idx in layers_to_collect: + collected.append(x) + x, attn_weights = block(x, sequence_id, output_attentions=output_attentions) + if output_attentions and attn_weights is not None: + all_attentions.append(attn_weights) + + norm_x = self.norm(x) + if len(self.blocks) in layers_to_collect: + collected.append(norm_x) + + attentions = tuple(all_attentions) if output_attentions else None + return norm_x, x, tuple(collected), attentions + + +# --------------------------------------------------------------------------- +# Pre-trained model base class +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCPreTrainedModel(PreTrainedModel): + """Base class for ESMC models. + + Handles weight initialisation and declares module-level capabilities. + """ + + config_class = ESMCConfig + base_model_prefix = "esmc" + supports_gradient_checkpointing = False + _supports_sdpa = True + _supports_flash_attn = True + _supports_attention_backend = True + _no_split_modules = ["UnifiedTransformerBlock"] + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + + def _init_weights(self, module: nn.Module): + std = self.config.initializer_range + if isinstance(module, nn.Linear): + module.weight.data.normal_(mean=0.0, std=std) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, RotaryEmbedding): + module.reset_parameters(device=self.device) + + +# --------------------------------------------------------------------------- +# Base encoder model +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCModel(ESMCPreTrainedModel): + """The bare ESMC encoder outputting raw hidden states. + + ESMC is a protein language model trained by EvolutionaryScale using a + masked-token objective over amino acid sequences. The architecture is a + standard Transformer encoder with RoPE positional embeddings, QK LayerNorm, + and SwiGLU feed-forward networks. + + Args: + config: An :class:`ESMCConfig` instance. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self._use_flash_attn = ( + _flash_attn_available and config._attn_implementation == "flash_attention_2" + ) + self.embed = nn.Embedding(config.vocab_size, config.d_model) + self.transformer = TransformerStack( + config.d_model, + config.n_heads, + config.n_layers, + use_flash_attn=self._use_flash_attn, + ) + self._sae_models: nn.ModuleDict = nn.ModuleDict() + self.post_init() + + def get_input_embeddings(self) -> nn.Embedding: + return self.embed + + def set_input_embeddings(self, value: nn.Embedding): + self.embed = value + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Register one or more SAEs obtained from an :class:`ESMCSAEModel`. + + Each is keyed by ``f"layer{N}"`` (the backbone-layer index ``N`` the + SAE is trained against, set by + :meth:`ESMCSAEModel.initialize_layers`). Attaching two SAEs for the + same backbone layer raises — only one SAE per layer can be active. + + Example:: + + sae = ESMCSAEModel.from_pretrained( + "biohub/esmc-600m-2024-12-sae-k64-codebook16384" + ) + sae.initialize_layers([27, 33]) + model.add_sae_models([sae.layers["27"], sae.layers["33"]]) + """ + for layer in sae_models: + assert isinstance(layer, _ESMCSAELayer), ( + f"Expected an SAE layer (model.layers['']), got " + f"{type(layer).__name__}." + ) + key = f"layer{int(layer.layer)}" + if key in self._sae_models: + raise ValueError( + f"An SAE is already registered at {key!r}. Only one SAE " + "per backbone layer can be active — pick a different " + "layer on one of them, or attach in a fresh model." + ) + self._sae_models[key] = layer + + _SAE_KEY_RE = re.compile(r"layer(\d+)") + + def _get_sae_layer_num_requested(self, model_name: str) -> int: + """Recover the backbone-layer index from a key written by + :meth:`add_sae_models` (``"layer{N}"`` → ``N``).""" + match = self._SAE_KEY_RE.fullmatch(model_name) + assert ( + match is not None + ), f"Unexpected SAE key {model_name!r}; expected 'layer{{N}}'." + return int(match.group(1)) + + def _validate_sae_inputs(self, input_ids: torch.Tensor) -> None: + assert torch.all(input_ids != self.config.mask_token_id), ( + "SAE inputs must not contain mask tokens. " + "SAEs were trained on unmasked sequences." + ) + + def _get_sae_outputs( + self, + hidden_states: torch.Tensor, + layers_to_collect: list[int], + token_mask: torch.Tensor, + normalize_sae: bool = False, + ) -> dict[str, torch.Tensor]: + """Run all registered SAEs and return their feature magnitudes. + + Args: + hidden_states: Stacked tensor of shape + ``(len(layers_to_collect), batch, seq_len, d_model)``. + layers_to_collect: The ESMC layer indices that were collected, + in the same order as the first dim of ``hidden_states``. + token_mask: Boolean mask ``(batch, seq_len)`` — ``True`` for + real (non-padding) tokens. + normalize_sae: When ``True``, scale features by ``idf / max`` + using the per-feature stats trained alongside each SAE. + """ + layer_to_idx = {layer: idx for idx, layer in enumerate(layers_to_collect)} + sae_outputs: dict[str, torch.Tensor] = {} + + for model_name, sae_module in self._sae_models.items(): + # `nn.ModuleDict` only stores `nn.Module`s at the type level; + # ``add_sae_models`` enforces that each entry is an ``_ESMCSAELayer``. + assert isinstance(sae_module, _ESMCSAELayer) + layer: _ESMCSAELayer = sae_module + requested_layer = self._get_sae_layer_num_requested(model_name) + layer_idx = layer_to_idx[requested_layer] + layer_states = hidden_states[layer_idx].clone().to(self.device) + + sae_out = layer.get_sae_output(layer_states, token_mask) + features = sae_out.feature_magnitudes.detach() + + if normalize_sae: + # ``register_buffer`` is typed as ``Tensor | Module`` on + # ``nn.Module``; narrow here since these are Tensors. + idf = cast(torch.Tensor, layer.idf) + max_val = cast(torch.Tensor, layer.max) + features = (features / max_val) * idf + + sae_outputs[model_name] = features.to_sparse() + + return sae_outputs + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + sequence_id: Optional[torch.Tensor] = None, + output_hidden_states: Optional[bool] = None, + output_attentions: Optional[bool] = None, + return_dict: Optional[bool] = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCOutput: + r""" + sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Integer chain-ID tensor for chain-aware attention masking. Tokens with the same + non-negative integer value can attend to each other; tokens with different values + cannot (cross-chain masking). Padding positions should be set to ``-1``. + When provided, ``attention_mask`` is ignored. The ``flash_attention_2`` backend + only supports single-chain inputs (all non-padding values must be ``0``); pass + multi-chain ``sequence_id`` with ``attn_implementation='sdpa'`` (or ``'eager'``). + output_attentions (`bool`, *optional*): + Whether to return the per-block attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the + attention probabilities are observable; raises on the + ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run any SAE models registered via :meth:`add_sae_models`. + Has no effect when no SAEs are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE feature magnitudes by ``idf / max`` (only + applied when the SAE's normalization buffers contain non-trivial values). + + Examples: + + ```python + >>> from transformers import AutoTokenizer, ESMCModel + + >>> model = ESMCModel.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> inputs = tokenizer(["MLKNVQVQLV"], return_tensors="pt") + >>> outputs = model(**inputs) + >>> outputs.last_hidden_state.shape + torch.Size([1, 12, 960]) + ``` + """ + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + output_sae = compute_sae and len(self._sae_models) > 0 + + # Determine which intermediate layers to collect. When SAEs are + # registered we must collect at least the layers they target, even if + # the caller did not ask for all hidden states. + if output_hidden_states: + layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) + elif output_sae: + layers_to_collect = sorted( + {self._get_sae_layer_num_requested(name) for name in self._sae_models} + ) + else: + layers_to_collect = [] + + user_supplied_sequence_id = sequence_id is not None + if sequence_id is not None: + bool_mask = sequence_id >= 0 + else: + if attention_mask is None: + attention_mask = input_ids != self.config.pad_token_id + assert attention_mask is not None + bool_mask = attention_mask.bool() + sequence_id = bool_mask.to(torch.long) - 1 + + x = self.embed(input_ids) + b, l_ = x.shape[:2] + + if self._use_flash_attn: + if user_supplied_sequence_id and (sequence_id > 0).any(): + raise ValueError( + "Multi-chain ``sequence_id`` (any value > 0) is not " + "supported with attn_implementation='flash_attention_2'. " + "Re-load the model with attn_implementation='sdpa' (or " + "'eager') for chain-aware attention masking." + ) + assert unpad_input is not None + x, indices, *_ = unpad_input(x, bool_mask) + else: + indices = None + + if self._use_flash_attn: + trans_seq_id = bool_mask + elif user_supplied_sequence_id: + trans_seq_id = sequence_id + elif bool_mask.all() and not output_attentions: + # Fused SDPA fast path (xformers / flash) is correct only when the + # mask is uniform; output_attentions forces the manual branch. + trans_seq_id = None + else: + trans_seq_id = sequence_id + last_hidden_state, _, collected, attentions = self.transformer( + x, + sequence_id=trans_seq_id, + layers_to_collect=layers_to_collect, + output_attentions=output_attentions, + ) + + if self._use_flash_attn: + assert indices is not None and pad_input is not None + last_hidden_state = pad_input(last_hidden_state, indices, b, l_) + collected = [pad_input(h, indices, b, l_) for h in collected] + + # Stack once; reused for both SAE and hidden-state output. + collected_tensor: torch.Tensor | None = ( + torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] + ) + + sae_outputs: dict[str, torch.Tensor] | None = None + if output_sae and collected_tensor is not None: + assert input_ids is not None + self._validate_sae_inputs(input_ids) + sae_outputs = self._get_sae_outputs( + collected_tensor, layers_to_collect, bool_mask, normalize_sae + ) + + hidden_states_tensor = collected_tensor if output_hidden_states else None + + if not return_dict: + return tuple( + v + for v in [ + last_hidden_state, + hidden_states_tensor, + sae_outputs, + attentions, + ] + if v is not None + ) + + return ESMCOutput( + last_hidden_state=last_hidden_state, + hidden_states=hidden_states_tensor, + sae_outputs=sae_outputs, + attentions=attentions, + ) + + +# --------------------------------------------------------------------------- +# LM head +# --------------------------------------------------------------------------- + + +def _esmc_lm_head( + d_model: int, output_dim: int, hidden_dim: int | None = None +) -> nn.Sequential: + """Linear → GELU → LayerNorm → Linear projection head for masked LM.""" + hidden_dim = hidden_dim if hidden_dim is not None else d_model + return nn.Sequential( + nn.Linear(d_model, hidden_dim), + nn.GELU(), + nn.LayerNorm(hidden_dim), + nn.Linear(hidden_dim, output_dim), + ) + + +# --------------------------------------------------------------------------- +# Masked language model +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCForMaskedLM(ESMCPreTrainedModel): + """ESMC with a masked language modelling head. + + This is the primary pre-training objective of ESMC. The LM head consists + of a single hidden layer with GELU activation followed by LayerNorm and a + linear projection to ``vocab_size``. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.esmc = ESMCModel(config) + self.lm_head = _esmc_lm_head(config.d_model, config.vocab_size) + self.post_init() + + def get_output_embeddings(self) -> nn.Linear: + return self.lm_head[-1] # type: ignore[return-value] + + def set_output_embeddings(self, new_embeddings: nn.Linear): + self.lm_head[-1] = new_embeddings + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Proxy to :meth:`ESMCModel.add_sae_models`.""" + self.esmc.add_sae_models(sae_models) + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + sequence_id: Optional[torch.Tensor] = None, + output_hidden_states: Optional[bool] = None, + output_attentions: Optional[bool] = None, + return_dict: Optional[bool] = None, + labels: Optional[torch.Tensor] = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: + r""" + sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Integer chain-ID tensor forwarded to the encoder for chain-aware + attention masking. See :meth:`ESMCModel.forward` for the encoding. + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for masked language modelling loss. Positions with label ``-100`` + are ignored. Other positions must be in ``[0, config.vocab_size)``. + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run registered SAE models. Has no effect when none are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE features by ``idf / max`` normalization buffers. + + Examples: + + ```python + >>> from transformers import AutoTokenizer, ESMCForMaskedLM + >>> import torch + + >>> model = ESMCForMaskedLM.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> inputs = tokenizer(["MLKNVQLV"], return_tensors="pt") + >>> outputs = model(**inputs) + >>> outputs.logits.shape + torch.Size([1, 11, 64]) + ``` + """ + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + encoder_outputs = self.esmc( + input_ids=input_ids, + attention_mask=attention_mask, + sequence_id=sequence_id, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=True, + compute_sae=compute_sae, + normalize_sae=normalize_sae, + ) + + logits = self.lm_head(encoder_outputs.last_hidden_state) + + loss: torch.Tensor | None = None + if labels is not None: + loss = CrossEntropyLoss(ignore_index=-100)( + logits.view(-1, self.config.vocab_size), labels.view(-1) + ) + + if not return_dict: + return tuple( + v + for v in [ + loss, + logits, + encoder_outputs.last_hidden_state, + encoder_outputs.hidden_states, + encoder_outputs.sae_outputs, + encoder_outputs.attentions, + ] + if v is not None + ) + + return ESMCMaskedLMOutput( + loss=loss, + logits=logits, + last_hidden_state=encoder_outputs.last_hidden_state, + hidden_states=encoder_outputs.hidden_states, + sae_outputs=encoder_outputs.sae_outputs, + attentions=encoder_outputs.attentions, + ) + + +# --------------------------------------------------------------------------- +# Classification heads +# --------------------------------------------------------------------------- + + +class _ESMCClassificationHead(nn.Module): + """Dense classification head applied to the ```` token representation.""" + + def __init__(self, config: ESMCConfig): + super().__init__() + self.dense = nn.Linear(config.d_model, config.d_model) + self.dropout = nn.Dropout(config.classifier_dropout) + self.out_proj = nn.Linear(config.d_model, config.num_labels) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + x = hidden_states[:, 0, :] # token + x = self.dropout(x) + x = torch.tanh(self.dense(x)) + x = self.dropout(x) + return self.out_proj(x) + + +# --------------------------------------------------------------------------- +# Sequence classification +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCForSequenceClassification(ESMCPreTrainedModel): + """ESMC with a sequence-level classification head. + + A linear layer is applied to the ```` token representation. + Supports regression (``num_labels == 1``), single-label classification, + and multi-label classification. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.num_labels = config.num_labels + self.esmc = ESMCModel(config) + self.classifier = _ESMCClassificationHead(config) + self.post_init() + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Proxy to :meth:`ESMCModel.add_sae_models`.""" + self.esmc.add_sae_models(sae_models) + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + output_hidden_states: Optional[bool] = None, + output_attentions: Optional[bool] = None, + return_dict: Optional[bool] = None, + labels: Optional[torch.Tensor] = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: + r""" + labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for sequence classification loss. Indices must be in + ``[0, config.num_labels - 1]``. For regression pass a float + tensor of shape ``(batch_size,)``. + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run registered SAE models. Has no effect when none are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE features by ``idf / max`` normalization buffers. + """ + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + encoder_outputs = self.esmc( + input_ids, + attention_mask=attention_mask, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=True, + compute_sae=compute_sae, + normalize_sae=normalize_sae, + ) + logits = self.classifier(encoder_outputs.last_hidden_state) + + loss: torch.Tensor | None = None + if labels is not None: + labels = labels.to(logits.device) + + if self.config.problem_type is None: + if self.num_labels == 1: + self.config.problem_type = "regression" + elif self.num_labels > 1 and labels.dtype in (torch.long, torch.int): + self.config.problem_type = "single_label_classification" + else: + self.config.problem_type = "multi_label_classification" + + if self.config.problem_type == "regression": + loss_fct = MSELoss() + loss = loss_fct( + logits.squeeze() if self.num_labels == 1 else logits, + labels.squeeze() if self.num_labels == 1 else labels, + ) + elif self.config.problem_type == "single_label_classification": + loss = CrossEntropyLoss()( + logits.view(-1, self.num_labels), labels.view(-1) + ) + elif self.config.problem_type == "multi_label_classification": + loss = BCEWithLogitsLoss()(logits, labels) + + if not return_dict: + return tuple( + v + for v in [ + loss, + logits, + encoder_outputs.last_hidden_state, + encoder_outputs.hidden_states, + encoder_outputs.sae_outputs, + encoder_outputs.attentions, + ] + if v is not None + ) + + return ESMCSequenceClassifierOutput( + loss=loss, + logits=logits, + last_hidden_state=encoder_outputs.last_hidden_state, + hidden_states=encoder_outputs.hidden_states, + sae_outputs=encoder_outputs.sae_outputs, + attentions=encoder_outputs.attentions, + ) + + +# --------------------------------------------------------------------------- +# Token classification +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCForTokenClassification(ESMCPreTrainedModel): + """ESMC with a per-token classification head. + + Useful for tasks such as secondary structure prediction, contact-map + prediction, or per-residue labelling. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.num_labels = config.num_labels + self.esmc = ESMCModel(config) + self.dropout = nn.Dropout(config.classifier_dropout) + self.classifier = nn.Linear(config.d_model, config.num_labels) + self.post_init() + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Proxy to :meth:`ESMCModel.add_sae_models`.""" + self.esmc.add_sae_models(sae_models) + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + output_hidden_states: Optional[bool] = None, + output_attentions: Optional[bool] = None, + return_dict: Optional[bool] = None, + labels: Optional[torch.Tensor] = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. + Positions with index ``-100`` are ignored in the loss. + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run registered SAE models. Has no effect when none are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE features by ``idf / max`` normalization buffers. + """ + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + encoder_outputs = self.esmc( + input_ids=input_ids, + attention_mask=attention_mask, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=True, + compute_sae=compute_sae, + normalize_sae=normalize_sae, + ) + + sequence_output = self.dropout(encoder_outputs.last_hidden_state) + logits = self.classifier(sequence_output) + + loss: torch.Tensor | None = None + if labels is not None: + loss = CrossEntropyLoss(ignore_index=-100)( + logits.view(-1, self.num_labels), labels.to(logits.device).view(-1) + ) + + if not return_dict: + return tuple( + v + for v in [ + loss, + logits, + encoder_outputs.last_hidden_state, + encoder_outputs.hidden_states, + encoder_outputs.sae_outputs, + encoder_outputs.attentions, + ] + if v is not None + ) + + return ESMCTokenClassifierOutput( + loss=loss, + logits=logits, + last_hidden_state=encoder_outputs.last_hidden_state, + hidden_states=encoder_outputs.hidden_states, + sae_outputs=encoder_outputs.sae_outputs, + attentions=encoder_outputs.attentions, + ) + + +__all__ = [ + "ESMCModel", + "ESMCForMaskedLM", + "ESMCForSequenceClassification", + "ESMCForTokenClassification", + "ESMCPreTrainedModel", +] diff --git a/src/transformers/models/esmc/modeling_esmc_sae.py b/src/transformers/models/esmc/modeling_esmc_sae.py new file mode 100644 index 000000000000..b706e50e9153 --- /dev/null +++ b/src/transformers/models/esmc/modeling_esmc_sae.py @@ -0,0 +1,363 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""PyTorch ESMC SAE (Sparse Autoencoder) model. + +* :class:`ESMCSAEModel` — the published HF container, one repo per + ``(backbone, codebook_dim, k)`` group. Each backbone layer ships as a + ``layer_{i}.safetensors`` shard; ``from_pretrained`` downloads the whole + snapshot but loads no weights — callers materialize the layers they need + via :meth:`initialize_layers`. Single-layer repos auto-load so bare + ``forward(x)`` works. +* :class:`_ESMCSAELayer` — internal ``nn.Module`` that holds the weights for + one ``(backbone, codebook_dim, k, layer)`` SAE. Not a published HF artifact; + obtained only via ``model.layers[""]``. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F +from safetensors.torch import load_file, save_file + +from ...modeling_outputs import ModelOutput # type: ignore[import] +from ...modeling_utils import PreTrainedModel # type: ignore[import] +from ...utils import auto_docstring # type: ignore[import] +from .configuration_esmc_sae import ESMCSAEConfig, ESMCSAEParams + + +@dataclass +@auto_docstring( + custom_intro=""" + Output type of [`ESMCSAEModel`]. + """ +) +class ESMCSAEOutput(ModelOutput): + feature_magnitudes: torch.Tensor + reconstruction_loss: Optional[torch.Tensor] = None + + def to_sparse(self) -> None: + self.feature_magnitudes = self.feature_magnitudes.to_sparse() + + +class _ESMCSAELayer(nn.Module): + """One backbone layer's SAE — internal building block of :class:`ESMCSAEModel`. + + Not exposed via ``AutoModel`` and not loadable on its own. Obtain one + via ``model.layers[""]`` after calling ``initialize_layers``. + """ + + def __init__(self, params: ESMCSAEParams): + super().__init__() + self.params = params + + self.W_enc = nn.Parameter(torch.empty(params.d_model, params.codebook_dim)) + self.W_dec = nn.Parameter(torch.empty(params.codebook_dim, params.d_model)) + self.b_dec = nn.Parameter(torch.zeros(params.d_model)) + # Per-feature normalization stats. Trained alongside the SAE for some + # variants; for variants that don't ship them, leaving these as ones + # makes ``_get_sae_outputs``'s ``features / max * idf`` a no-op. + self.register_buffer("idf", torch.ones(params.codebook_dim)) + self.register_buffer("max", torch.ones(params.codebook_dim)) + + @property + def layer(self) -> int: + """Backbone-layer index this SAE is trained against.""" + return self.params.layer + + def forward(self, x: torch.Tensor, **_kwargs: object) -> ESMCSAEOutput: + del _kwargs + x = self._zscore_normalize_representation(x) + + x_with_pre_encoder_bias = x - self.b_dec + preactivations = F.relu(x_with_pre_encoder_bias @ self.W_enc) + + topk = torch.topk(preactivations, self.params.k, dim=-1) + feature_magnitudes = torch.zeros_like(preactivations).scatter( + -1, topk.indices, topk.values + ) + + reconstructed = feature_magnitudes @ self.W_dec + self.b_dec + + reconstruction_loss = (reconstructed - x).pow(2).mean(dim=-1) + + return ESMCSAEOutput( + feature_magnitudes=feature_magnitudes, + reconstruction_loss=reconstruction_loss, + ) + + def get_sae_output( + self, layer_states: torch.Tensor, token_mask: torch.Tensor + ) -> ESMCSAEOutput: + _, _, v_len = layer_states.shape + nonpad_states = layer_states[token_mask].view(-1, v_len) + return self(nonpad_states) + + def _zscore_normalize_representation(self, x: torch.Tensor) -> torch.Tensor: + x_mean = x.mean(dim=-1, keepdim=True) + x = x - x_mean + x_std = x.std(dim=-1, keepdim=True) + return x / (x_std + 1e-5) + + +@auto_docstring +class ESMCSAEPreTrainedModel(PreTrainedModel): + config_class = ESMCSAEConfig + base_model_prefix = "esmc_sae" + + +@auto_docstring( + custom_intro=""" + HF container holding one SAE per backbone layer, all sharing the same + ``(d_model, codebook_dim, k)``. + + ``from_pretrained`` downloads the entire repo (every ``layer_{i}.safetensors``) + into the local HF cache but does **not** load any weights into memory. + Callers materialize the layers they actually need by calling + :meth:`initialize_layers`. The full set is available on disk after the + first call, so subsequent layer switches read from the local cache without + re-downloading. + + Examples:: + + model = ESMCSAEModel.from_pretrained( + "biohub/esmc-6b-2024-12-sae-k64-codebook16384" + ) + model.initialize_layers([60]) # ~2.5 GB into memory + out = model(layer_states, layer=60) # forward through layer 60 + model.initialize_layers([45]) # add layer 45 (cached locally) + model.release_layer(60) # free layer 60 + """ +) +class ESMCSAEModel(ESMCSAEPreTrainedModel): + def __init__(self, config: ESMCSAEConfig): + super().__init__(config) + # Layers are populated lazily by ``initialize_layers``; the container + # starts empty so ``from_pretrained`` doesn't materialize hundreds of + # GB of unused parameters. + self.layers = nn.ModuleDict() + # Zero-element buffer that rides along with ``.to(device/dtype)``. + # ``initialize_layers`` reads its current device/dtype so SAEs added + # after ``model.to("cuda")`` land on CUDA without re-passing ``device=``. + self.register_buffer("_device_marker", torch.empty(0), persistent=False) + self._snapshot_dir: Optional[str] = None + self.post_init() + + @classmethod + def from_pretrained( # type: ignore[override] + cls, pretrained_model_name_or_path: str | os.PathLike, *model_args, **kwargs + ) -> "ESMCSAEModel": + """Download (or reuse cached) the full repo and return the model. + + By default no weights are read into memory and the caller must invoke + :meth:`initialize_layers` before running :meth:`forward`. The single + exception is when the repo ships exactly one layer: that layer is + auto-loaded (honoring ``torch_dtype`` / ``device`` if passed) so the + bare ``forward(x)`` call just works. + + Honored kwargs: ``revision``, ``cache_dir``, ``token``, + ``allow_patterns``, ``local_files_only``, ``force_download`` (forwarded + to ``snapshot_download``); ``torch_dtype`` and ``device`` (used by the + single-layer auto-load path; otherwise pass them to + :meth:`initialize_layers`). Behavioral kwargs that imply work we do + not perform (``device_map``, ``low_cpu_mem_usage``, + ``quantization_config``, ``attn_implementation``) raise so the user + isn't silently misled. Other HF housekeeping kwargs (``config``, + ``trust_remote_code``, ``adapter_kwargs``, …) are accepted and + ignored — they only matter for the standard loader, which we bypass. + """ + del model_args + torch_dtype = kwargs.pop("torch_dtype", None) + device = kwargs.pop("device", None) + local_dir = _resolve_snapshot_dir(pretrained_model_name_or_path, kwargs) + unsupported = { + "device_map", + "low_cpu_mem_usage", + "quantization_config", + "attn_implementation", + "max_memory", + "offload_folder", + "offload_state_dict", + } & kwargs.keys() + if unsupported: + raise TypeError( + f"Unsupported kwargs to ESMCSAEModel.from_pretrained: " + f"{sorted(unsupported)}. The standard HF loader is bypassed —" + " call initialize_layers(..., device=, dtype=) instead." + ) + config = ESMCSAEConfig.from_pretrained(local_dir) + model = cls(config) + model._snapshot_dir = str(local_dir) + if device is not None: + model.to(device) + if torch_dtype is not None: + model.to(torch_dtype) + if len(config.available_layers) == 1: + model.initialize_layers(list(config.available_layers)) + return model + + def initialize_layers( + self, + layers: list[int], + *, + device: torch.device | str | None = None, + dtype: torch.dtype | None = None, + ) -> None: + """Load the requested layers from the local snapshot into memory. + + Layers already present in :attr:`self.layers` are skipped — calling + ``initialize_layers([23])`` twice is idempotent. ``device`` / ``dtype`` + default to wherever the model itself lives (via the ``_device_marker`` + buffer that moves with ``.to(...)``), so the common pattern of + ``model.to("cuda"); model.initialize_layers([7])`` Just Works. + """ + assert self._snapshot_dir is not None, ( + "ESMCSAEModel has no snapshot directory — call " + "from_pretrained first, or set _snapshot_dir manually." + ) + if device is None: + device = self._device_marker.device + if dtype is None: + dtype = self._device_marker.dtype + snapshot_dir = Path(self._snapshot_dir) + available = set(self.config.available_layers) + for layer_idx in layers: + key = str(layer_idx) + if key in self.layers: + continue + if layer_idx not in available: + raise KeyError( + f"Layer {layer_idx} is not in this repo. " + f"available_layers={sorted(available)}" + ) + shard = snapshot_dir / f"layer_{layer_idx}.safetensors" + if not shard.exists(): + raise FileNotFoundError( + f"Missing layer file {shard} — config lists layer " + f"{layer_idx} as available but the shard is not on disk." + ) + params = ESMCSAEParams( + d_model=self.config.d_model, + codebook_dim=self.config.codebook_dim, + k=self.config.k, + layer=layer_idx, + ) + # Build on the meta device so we don't allocate weights that + # ``load_state_dict`` would immediately overwrite. + with torch.device("meta"): + layer = _ESMCSAELayer(params) + layer.to_empty(device=device) + layer.load_state_dict(load_file(str(shard))) + layer.to(dtype=dtype) + self.layers[key] = layer + + def release_layer(self, layer: int) -> None: + """Drop the named layer from memory. No-op if not loaded.""" + key = str(layer) + if key in self.layers: + del self.layers[key] + + def loaded_layers(self) -> list[int]: + """Sorted list of layer indices currently materialized in memory.""" + return sorted(int(k) for k in self.layers.keys()) + + def forward( + self, x: torch.Tensor, layer: int | None = None, **kwargs: object + ) -> ESMCSAEOutput: + if layer is None: + if len(self.layers) == 1: + # Unambiguous: exactly one layer loaded → use it. + ((_only_key, only_layer),) = self.layers.items() + return only_layer(x, **kwargs) + if len(self.layers) == 0: + raise RuntimeError( + "No layers loaded — call " + f"initialize_layers([...]) first. " + f"available_layers={self.config.available_layers}" + ) + raise RuntimeError( + "Multiple layers are loaded — please select one via " + f"forward(x, layer=). Loaded layers: {self.loaded_layers()}" + ) + key = str(layer) + if key not in self.layers: + raise KeyError( + f"Layer {layer} is not loaded. Call " + f"initialize_layers([{layer}]) first. Loaded layers: " + f"{self.loaded_layers()}" + ) + return self.layers[key](x, **kwargs) + + def save_pretrained( # type: ignore[override] + self, save_directory: str | os.PathLike, *args, **kwargs + ) -> None: + """Write ``config.json`` plus one ``layer_{i}.safetensors`` per loaded layer. + + Only layers currently in :attr:`self.layers` are written. + ``available_layers`` in the saved config is synced to what's actually + on disk so a ``release_layer`` + ``save_pretrained`` round-trip never + advertises a layer whose shard is missing. + """ + del args, kwargs + save_directory = Path(save_directory) + save_directory.mkdir(parents=True, exist_ok=True) + # Sync available_layers to what we're about to write — never advertise + # a layer that isn't on disk in this repo. + self.config.available_layers = self.loaded_layers() + self.config.save_pretrained(str(save_directory)) + for key, layer in self.layers.items(): + shard = save_directory / f"layer_{key}.safetensors" + save_file( + { + k: v.detach().cpu().contiguous() + for k, v in layer.state_dict().items() + }, + str(shard), + ) + + +def _resolve_snapshot_dir( + pretrained_model_name_or_path: str | os.PathLike, kwargs: dict +) -> str: + """Local dir → return as-is; hub id → ``snapshot_download`` it. + + A directory only counts as "local" if it actually contains ``config.json``, + so a stale subdir named like a hub id (``./biohub/esmc-...``) + doesn't accidentally shadow the hub fetch. + + Pops the standard ``snapshot_download`` keyword args from ``kwargs`` so + callers can forward them via ``from_pretrained``. + """ + path = Path(pretrained_model_name_or_path) + if path.is_dir() and (path / "config.json").exists(): + return str(path) + from huggingface_hub import snapshot_download + + return snapshot_download( + repo_id=str(pretrained_model_name_or_path), + revision=kwargs.pop("revision", None), + cache_dir=kwargs.pop("cache_dir", None), + token=kwargs.pop("token", None), + allow_patterns=kwargs.pop("allow_patterns", None), + local_files_only=kwargs.pop("local_files_only", False), + force_download=kwargs.pop("force_download", False), + ) + + +__all__ = ["ESMCSAEModel", "ESMCSAEOutput", "ESMCSAEPreTrainedModel"] diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py new file mode 100644 index 000000000000..248577919450 --- /dev/null +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -0,0 +1,217 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tokenization classes for ESMC.""" + +from tokenizers import AddedToken, Tokenizer +from tokenizers.models import BPE +from tokenizers.processors import TemplateProcessing + +from ...tokenization_utils_fast import PreTrainedTokenizerFast # type: ignore[import] +from ...utils import logging # type: ignore[import] + +logger = logging.get_logger(__name__) + +VOCAB_FILES_NAMES = {"vocab_file": "vocab.txt", "tokenizer_file": "tokenizer.json"} + +# Canonical amino acid vocabulary used by all ESMC checkpoints. +# Indices must be kept stable — they are hard-coded into model weights. +SEQUENCE_VOCAB = [ + "", # 0 + "", # 1 + "", # 2 + "", # 3 + "L", # 4 + "A", # 5 + "G", # 6 + "V", # 7 + "S", # 8 + "E", # 9 + "R", # 10 + "T", # 11 + "I", # 12 + "D", # 13 + "P", # 14 + "K", # 15 + "Q", # 16 + "N", # 17 + "F", # 18 + "Y", # 19 + "M", # 20 + "H", # 21 + "W", # 22 + "C", # 23 + "X", # 24 ambiguous amino acid + "B", # 25 Asp/Asn ambiguity + "U", # 26 selenocysteine + "Z", # 27 Glu/Gln ambiguity + "O", # 28 pyrrolysine + ".", # 29 gap + "-", # 30 insertion + "|", # 31 chain-break + "", # 32 +] + + +class ESMCTokenizer(PreTrainedTokenizerFast): + r""" + Construct an ESMC tokenizer. + + This tokenizer is a character-level tokenizer backed by the HuggingFace `tokenizers` library. + It wraps every sequence with ```` and ```` tokens and supports a ``|`` chain-break + token for multi-chain inputs. + + Args: + unk_token (`str`, *optional*, defaults to ``""``): + The unknown token. + cls_token (`str`, *optional*, defaults to ``""``): + The classification token (prepended to every sequence). + pad_token (`str`, *optional*, defaults to ``""``): + The padding token. + mask_token (`str`, *optional*, defaults to ``""``): + The mask token, used for masked language modelling. + eos_token (`str`, *optional*, defaults to ``""``): + The end-of-sequence token (appended to every sequence). + chain_break_token (`str`, *optional*, defaults to ``"|"``): + Token inserted between chains in multi-chain protein inputs. + + Examples: + + ```python + >>> from transformers import ESMCTokenizer + + >>> tokenizer = ESMCTokenizer() + >>> tokenizer("ACDEFGHIKLMNPQRSTVWY")["input_ids"] + [0, 5, 23, 13, 18, 9, 6, 21, 12, 15, 20, 17, 14, 16, 10, 8, 11, 7, 22, 19, 2] + ``` + """ + + vocab_files_names = VOCAB_FILES_NAMES + model_input_names = ["input_ids", "attention_mask"] + + def __init__( + self, + unk_token="", + cls_token="", + pad_token="", + mask_token="", + eos_token="", + chain_break_token="|", + **kwargs, + ): + all_tokens = SEQUENCE_VOCAB + token_to_id = {tok: ind for ind, tok in enumerate(all_tokens)} + + # Normalise: always work with plain strings + unk_token = self._ensure_str(unk_token) + cls_token = self._ensure_str(cls_token) + pad_token = self._ensure_str(pad_token) + mask_token = self._ensure_str(mask_token) + eos_token = self._ensure_str(eos_token) + chain_break_token = self._ensure_str(chain_break_token) + + # A character-level tokenizer is equivalent to BPE with no merges. + bpe = BPE(token_to_id, merges=[], unk_token=unk_token) + tokenizer = Tokenizer(bpe) + + special_tokens = [ + cls_token, + pad_token, + mask_token, + eos_token, + chain_break_token, + ] + tokenizer.add_special_tokens(special_tokens) + + # Automatically wrap every encoded sequence with . + tokenizer.post_processor = TemplateProcessing( + single=f"{cls_token} $A {eos_token}", + special_tokens=[ + (cls_token, tokenizer.token_to_id(cls_token)), + (eos_token, tokenizer.token_to_id(eos_token)), + ], + ) + + # Expose chain-break token as an additional special token so it is + # preserved during encode/decode and can be looked up easily. + kwargs.setdefault("additional_special_tokens", []) + if chain_break_token not in kwargs["additional_special_tokens"]: + kwargs["additional_special_tokens"] = list( + kwargs["additional_special_tokens"] + ) + [chain_break_token] + + # Keep reference before super().__init__ so properties below work. + self._chain_break_token = chain_break_token + + super().__init__( + tokenizer_object=tokenizer, + unk_token=unk_token, + cls_token=cls_token, + pad_token=pad_token, + mask_token=mask_token, + eos_token=eos_token, + **kwargs, + ) + + # ------------------------------------------------------------------ + # bos / cls aliases + # The model uses as the sequence-start token; the HF base class + # expects `bos_token`. We alias them to avoid confusion. + # ------------------------------------------------------------------ + + @property + def bos_token(self): + return self.cls_token + + @property + def bos_token_id(self): + return self.cls_token_id + + # ------------------------------------------------------------------ + # Chain-break token + # ------------------------------------------------------------------ + + @property + def chain_break_token(self) -> str: + return self._chain_break_token + + @property + def chain_break_token_id(self) -> int: + token_id = self.convert_tokens_to_ids(self._chain_break_token) + assert isinstance(token_id, int) + return token_id + + # ------------------------------------------------------------------ + # Convenience helpers used by downstream code + # ------------------------------------------------------------------ + + @property + def all_token_ids(self): + return list(range(self.vocab_size)) + + @property + def special_token_ids(self): + return self.all_special_ids + + # ------------------------------------------------------------------ + # Internal utilities + # ------------------------------------------------------------------ + + @staticmethod + def _ensure_str(token) -> str: + if isinstance(token, AddedToken): + return token.content + return str(token) + + +__all__ = ["ESMCTokenizer"] diff --git a/src/transformers/models/esmfold2/__init__.py b/src/transformers/models/esmfold2/__init__.py new file mode 100644 index 000000000000..6b245e85ea05 --- /dev/null +++ b/src/transformers/models/esmfold2/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TYPE_CHECKING + +from ...utils import _LazyModule # type: ignore[import] +from ...utils.import_utils import define_import_structure # type: ignore[import] + +if TYPE_CHECKING: + from .configuration_esmfold2 import * # noqa: F403 + from .modeling_esmfold2 import * # noqa: F403 + from .modeling_esmfold2_experimental import * # noqa: F403 +else: + import sys + + _file = globals()["__file__"] + sys.modules[__name__] = _LazyModule( + __name__, _file, define_import_structure(_file), module_spec=__spec__ + ) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py new file mode 100644 index 000000000000..2a8a215cf9a8 --- /dev/null +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -0,0 +1,298 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""ESMFold2 model configuration.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field + +from ...configuration_utils import PretrainedConfig # type: ignore[import] + +# --------------------------------------------------------------------------- +# Nested dataclass configs +# --------------------------------------------------------------------------- + +_DEFAULT_ESMC_HF_REPO = "biohub/ESMC-6B" + + +@dataclass +class MSAEncoderConfig: + """Config for the optional MSA encoder module (Large MSA models only).""" + + enabled: bool = False + d_msa: int = 128 + d_hidden: int = 32 + n_layers: int = 4 + n_heads_msa: int = 8 + msa_head_width: int = 32 + + +@dataclass +class ParcaeConfig: + """Release-only config for the parcae diffusion-loop scheduler.""" + + enabled: bool = True + poisson_mean: float = 3.0 + min_steps: int = 1 + max_steps: int | None = 6 + coda_n_layers: int = 2 + + +@dataclass +class LMEncoderConfig: + """Release-only config for the LM-side pair encoder.""" + + enabled: bool = True + n_layers: int = 4 + lm_dropout: float = 0.25 + per_loop_lm_dropout: bool = True + + +@dataclass +class AtomAttentionConfig: + """Config for SWA atom encoder/decoder with 3D RoPE.""" + + d_atom: int = 128 + d_token: int = 768 + n_blocks: int = 3 + n_heads: int = 4 + swa_window_size: int = 128 + expansion_ratio: int = 2 + # 3D RoPE config + spatial_rope_base_frequency: float = 20.0 + n_spatial_rope_pairs_per_axis: int = 2 + n_uid_rope_pairs: int = 10 + uid_rope_base_frequency: float = 10000.0 + + +@dataclass +class FoldingTrunkConfig: + n_layers: int = 24 + n_heads: int = 8 + dropout: float = 0.0 + + +@dataclass +class InputsEmbedderConfig: + d_inputs: int = 451 + atom_encoder: AtomAttentionConfig = field(default_factory=AtomAttentionConfig) + + def __post_init__(self): + if isinstance(self.atom_encoder, dict): + self.atom_encoder = AtomAttentionConfig(**self.atom_encoder) + + +@dataclass +class DiffusionModuleConfig: + """Config for the DiffusionModule.""" + + sigma_data: float = 16.0 + c_atom: int = 128 + c_token: int = 768 + c_z: int = 256 + c_s_inputs: int = 451 + fourier_dim: int = 256 + relpos_r_max: int = 32 + relpos_s_max: int = 2 + atom_num_blocks: int = 3 + atom_num_heads: int = 4 + token_num_blocks: int = 12 + token_num_heads: int = 16 + transition_multiplier: int = 2 + + +@dataclass +class DiffusionStructureHeadConfig: + """Config for the diffusion-based structure prediction head.""" + + diffusion_module: DiffusionModuleConfig = field( + default_factory=DiffusionModuleConfig + ) + distogram_bins: int = 128 + + # Training noise: sigma ~ sigma_data * exp(mu + sigma * N(0,1)) + train_noise_log_mean: float = -1.2 + train_noise_log_std: float = 1.5 + + # Sampling defaults (ODE) + gamma_0: float = 0.605 + gamma_min: float = 1.107 + noise_scale: float = 0.0 + step_scale: float = 1.0 + + # Inference schedule defaults + inference_s_max: float = 160.0 + inference_s_min: float = 4e-4 + inference_p: float = 8.0 + inference_num_steps: int = 68 + + def __post_init__(self): + if isinstance(self.diffusion_module, dict): + self.diffusion_module = DiffusionModuleConfig(**self.diffusion_module) + + +@dataclass +class ConfidenceHeadConfig: + enabled: bool = True + num_plddt_bins: int = 50 + num_pde_bins: int = 64 + num_pae_bins: int = 64 + min_dist: float = 2.0 + max_dist: float = 52.0 + distogram_bins: int = 128 + folding_trunk: FoldingTrunkConfig = field( + default_factory=lambda: FoldingTrunkConfig(n_layers=4) + ) + + def __post_init__(self): + if isinstance(self.folding_trunk, dict): + self.folding_trunk = FoldingTrunkConfig(**self.folding_trunk) + + +# --------------------------------------------------------------------------- +# Top-level config +# --------------------------------------------------------------------------- + + +class ESMFold2Config(PretrainedConfig): + """ + Configuration for the ESMFold2 structure prediction model. + + Uses SWA atom encoders with 3D RoPE, a diffusion transformer, + a folding trunk, and an ESMC 6B PLM backbone. + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control + the model outputs. Read the documentation from [`PretrainedConfig`] for more + information. + + Args: + d_single (`int`, defaults to 384): + Dimensionality of single (per-residue) representations. + d_pair (`int`, defaults to 256): + Dimensionality of pair (residue-residue) representations. + n_relative_residx_bins (`int`, defaults to 32): + Number of bins for relative residue index encoding. + n_relative_chain_bins (`int`, defaults to 2): + Number of bins for relative chain encoding. + num_loops (`int`, defaults to 10): + Number of trunk loops for iterative refinement. + num_diffusion_samples (`int`, defaults to 8): + Number of parallel structure predictions to generate. + lm_dropout (`float`, defaults to 0.0): + Dropout probability on LM pair embeddings. When > 0, dropout is + applied with ``training=True`` (including at inference) to match + the experimental training recipe used by binder design. + force_lm_dropout_during_inference (`bool`, defaults to False): + When True, apply ``lm_dropout`` even when ``model.eval()`` and + ``lm_dropout`` > 0. Binder-design loads set this to True. + disable_msa_features (`bool`, defaults to False): + When True, zero out MSA-derived ``profile`` and ``deletion_mean`` + before the inputs embedder (experimental medium/large checkpoints). + inputs (`InputsEmbedderConfig`): + Configuration for the inputs embedder module. + folding_trunk (`FoldingTrunkConfig`): + Configuration for the folding trunk. + structure_head (`DiffusionStructureHeadConfig`): + Configuration for the diffusion-based structure prediction head. + confidence_head (`ConfidenceHeadConfig`): + Configuration for the confidence prediction head. + + Examples: + + ```python + >>> from transformers import ESMFold2Config, ESMFold2ExperimentalModel + + >>> # Initializing an ESMFold2 configuration + >>> configuration = ESMFold2Config(type="experimental") + + >>> # Initializing a model (with random weights) from the configuration + >>> model = ESMFold2ExperimentalModel(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ``` + """ + + model_type = "esmfold2" + has_no_defaults_at_init = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.type: str = kwargs["type"] + if self.type not in ("release", "experimental"): + raise ValueError( + f"ESMFold2Config.type must be 'release' or 'experimental', " + f"got {self.type!r}" + ) + + # Top-level scalar fields + self.d_single: int = kwargs.get("d_single", 384) + self.d_pair: int = kwargs.get("d_pair", 256) + self.n_relative_residx_bins: int = kwargs.get("n_relative_residx_bins", 32) + self.n_relative_chain_bins: int = kwargs.get("n_relative_chain_bins", 2) + self.num_loops: int = kwargs.get("num_loops", 10) + self.num_diffusion_samples: int = kwargs.get("num_diffusion_samples", 8) + # If True, ``profile`` / ``deletion_mean`` are zeroed before the inputs + # embedder. + self.disable_msa_features: bool = kwargs.get("disable_msa_features", False) + self.lm_dropout: float = kwargs.get("lm_dropout", 0.0) + self.force_lm_dropout_during_inference: bool = kwargs.get( + "force_lm_dropout_during_inference", False + ) + + self.lm_d_model: int = kwargs.get("lm_d_model", 2560) + self.lm_num_layers: int = kwargs.get("lm_num_layers", 80) + # Required, no default — every shipped HF export must name its ESMC backbone. + self.esmc_id: str = kwargs.get("esmc_id", _DEFAULT_ESMC_HF_REPO) + + def _init_nested(cls, val): + if isinstance(val, cls): + return val + if isinstance(val, dict): + return cls(**val) + return cls() + + self.inputs = _init_nested(InputsEmbedderConfig, kwargs.get("inputs")) + self.folding_trunk = _init_nested( + FoldingTrunkConfig, kwargs.get("folding_trunk") + ) + self.structure_head = _init_nested( + DiffusionStructureHeadConfig, kwargs.get("structure_head") + ) + self.confidence_head = _init_nested( + ConfidenceHeadConfig, kwargs.get("confidence_head") + ) + self.msa_encoder = _init_nested(MSAEncoderConfig, kwargs.get("msa_encoder")) + # Release-only modules — ignored when ``type == "experimental"``. + self.parcae = _init_nested(ParcaeConfig, kwargs.get("parcae")) + self.lm_encoder = _init_nested(LMEncoderConfig, kwargs.get("lm_encoder")) + # If True, MSA encoder output replaces the pair stream; if False, it is added. + self.msa_encoder_overwrite: bool = bool( + kwargs.get("msa_encoder_overwrite", True) + ) + + def to_dict(self): + output = super().to_dict() + output["inputs"] = asdict(self.inputs) + output["folding_trunk"] = asdict(self.folding_trunk) + output["structure_head"] = asdict(self.structure_head) + output["confidence_head"] = asdict(self.confidence_head) + output["msa_encoder"] = asdict(self.msa_encoder) + output["parcae"] = asdict(self.parcae) + output["lm_encoder"] = asdict(self.lm_encoder) + return output + + +__all__ = ["ESMFold2Config", "MSAEncoderConfig", "ParcaeConfig", "LMEncoderConfig"] diff --git a/src/transformers/models/esmfold2/distributed/__init__.py b/src/transformers/models/esmfold2/distributed/__init__.py new file mode 100644 index 000000000000..1854a7aa0afb --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/__init__.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""2D context-parallel distributed extensions for ESMFold2.""" + +from projects.huggingface.transformers.models.esmfold2.distributed.manager import ( + DistributedManager, +) +from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.pairformer import ( + FoldingTrunkDistributed, +) +from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( + TrunkCPWrapper, + wrap_model_with_cp_trunks, +) + +__all__ = [ + "DistributedManager", + "FoldingTrunkDistributed", + "TrunkCPWrapper", + "wrap_model_with_cp_trunks", +] diff --git a/src/transformers/models/esmfold2/distributed/comm.py b/src/transformers/models/esmfold2/distributed/comm.py new file mode 100644 index 000000000000..6a8f803ebb21 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/comm.py @@ -0,0 +1,384 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Communication primitives for 2D context-parallel distributed operations.""" + +from typing import Optional + +import torch +import torch.distributed as dist + +from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( + LayoutMap, + get_group_rank_from_axial_shift, +) + + +class One2OneComm: + """Point-to-point communication with parity-based deadlock avoidance.""" + + def __init__( + self, + group: dist.ProcessGroup, + rank_send_to: int, + rank_recv_from: int, + parity: Optional[bool] = None, + ): + self.group = group + self.rank = dist.get_rank(self.group) + self.world_size = dist.get_world_size(self.group) + + if rank_send_to >= self.world_size: + raise ValueError(f"rank_send_to >= world_size {self.world_size}") + if rank_recv_from >= self.world_size: + raise ValueError(f"rank_recv_from >= world_size {self.world_size}") + + is_self_send = rank_send_to == self.rank + is_self_recv = rank_recv_from == self.rank + if is_self_send != is_self_recv: + raise ValueError( + "Asymmetric send/recv not supported: " + f"is_self_send={is_self_send}, is_self_recv={is_self_recv}" + ) + self.is_self_comm = is_self_send + self._rank_in_group_send_to = rank_send_to + self._rank_in_group_recv_from = rank_recv_from + self.parity = parity + + if not self.is_self_comm: + self.rank_send_to = dist.get_global_rank(self.group, rank_send_to) + self.rank_recv_from = dist.get_global_rank(self.group, rank_recv_from) + if self.parity is None: + self.parity = bool(self.rank % 2) + self._queue_send_recv = [] + self._work_to_finish = None + + def __deepcopy__(self, memo): + return One2OneComm( + self.group, + self._rank_in_group_send_to, + self._rank_in_group_recv_from, + self.parity, + ) + + def _prep_batch_isend_irecv( + self, to_send: torch.Tensor, to_recv: Optional[torch.Tensor] = None + ) -> torch.Tensor: + if self.is_self_comm: + if to_recv is None: + return to_send.detach().clone() + to_recv.copy_(to_send) + return to_recv + + ans = torch.empty_like(to_send) if to_recv is None else to_recv + if self.parity: + send_op = dist.P2POp( + dist.isend, to_send, self.rank_send_to, group=self.group + ) + recv_op = dist.P2POp(dist.irecv, ans, self.rank_recv_from, group=self.group) + self._queue_send_recv.append(send_op) + self._queue_send_recv.append(recv_op) + else: + recv_op = dist.P2POp(dist.irecv, ans, self.rank_recv_from, group=self.group) + send_op = dist.P2POp( + dist.isend, to_send, self.rank_send_to, group=self.group + ) + self._queue_send_recv.append(recv_op) + self._queue_send_recv.append(send_op) + return ans + + def _dispatch(self): + if self.is_self_comm: + return + if self._work_to_finish is not None: + raise RuntimeError("Unfinished communication in queue; cannot dispatch new") + self._work_to_finish = dist.batch_isend_irecv(self._queue_send_recv) + + def wait_until_finished(self): + if self.is_self_comm: + return + if self._work_to_finish is None: + raise RuntimeError("Cannot wait without unfinished communication") + for work in self._work_to_finish: + work.wait() + self._work_to_finish = None + self._queue_send_recv = [] + + def enqueue_to_dispatch( + self, to_send: torch.Tensor, to_recv: Optional[torch.Tensor] = None + ) -> torch.Tensor: + recv = self._prep_batch_isend_irecv(to_send, to_recv) + if self.is_self_comm: + return recv + self._dispatch() + return recv + + +class TransposeComm(One2OneComm): + """Transposes data between (i,j) and (j,i) on a square grid.""" + + def __init__(self, process_group: dist.ProcessGroup, group_layout: LayoutMap): + if group_layout.shape is None: + raise ValueError("group_layout must have a shape") + self.world_size = dist.get_world_size(process_group) + if self.world_size != group_layout.numel: + raise ValueError("Inconsistent world_size and group_layout.numel") + if len(group_layout.shape) != 2: + raise ValueError(f"{self.__class__} only supports 2D group layout") + if group_layout.shape[0] != group_layout.shape[1]: + raise ValueError(f"group_layout.shape {group_layout.shape} is not square") + + self.group_layout = group_layout + self.global_rank = dist.get_rank() + self.group_rank = dist.get_rank(process_group) + self.rank_coords: tuple[int, ...] = self.group_layout.unravel(self.group_rank) + + transpose_group_rank = self.group_layout(self.rank_coords[::-1]) + self.transpose_rank = dist.get_global_rank(process_group, transpose_group_rank) + self.parity_transpose = self.rank_coords[0] < self.rank_coords[1] + super().__init__( + process_group, + transpose_group_rank, + transpose_group_rank, + parity=self.parity_transpose, + ) + + def __deepcopy__(self, memo): + return TransposeComm(self.group, self.group_layout) + + +def ternary_parity(my_rank: int, send_rank: int, recv_rank: int) -> bool: + """Parity to avoid deadlocks: True if my_rank < min(send, recv).""" + return my_rank < min(send_rank, recv_rank) + + +class Ring2DComm: + """Ring communication on a 2D grid for TriangleMult and similar operations. + + Sets up: + - Transpose communication (send (i,j) to (j,i)) + - Row-wise ring (left shift per row) + - Column-wise ring (up shift per column) + - Initial offset shifts (row i shifts left by i; col j shifts up by j) + """ + + def __init__( + self, + group_2d: dist.ProcessGroup, + group_col: dist.ProcessGroup, + group_layout: LayoutMap, + ): + self.group_2d = group_2d + self.group_col = group_col + self.group_layout = group_layout + ranks_group_2d = set(dist.get_process_group_ranks(self.group_2d)) + ranks_group_col = set(dist.get_process_group_ranks(self.group_col)) + + if not ranks_group_col.issubset(ranks_group_2d): + raise ValueError("group_col ranks are not a subset of group_2d ranks") + + self.size_2d = dist.get_world_size(self.group_2d) + if self.size_2d != self.group_layout.numel: + raise ValueError( + f"group_2d size {self.size_2d} != group_layout.numel {self.group_layout.numel}" + ) + if self.group_layout.shape[0] != self.group_layout.shape[1]: + raise ValueError( + f"group_layout.shape {self.group_layout.shape} is not square" + ) + + self.rank_2d = dist.get_rank(self.group_2d) + self.coord_2d = self.group_layout.unravel(self.rank_2d) + + self.comm_2d_trans = TransposeComm(self.group_2d, self.group_layout) + + # Initial row shift: row i shifts left by i + self.send_rank_row_init = get_group_rank_from_axial_shift( + self.coord_2d, 1, -self.coord_2d[0], self.group_layout + ) + self.recv_rank_row_init = get_group_rank_from_axial_shift( + self.coord_2d, 1, self.coord_2d[0], self.group_layout + ) + self.comm_row_init = One2OneComm( + self.group_2d, + self.send_rank_row_init, + self.recv_rank_row_init, + parity=ternary_parity( + self.rank_2d, self.send_rank_row_init, self.recv_rank_row_init + ), + ) + + # Subsequent row shifts: left by 1 + self.send_rank_row = get_group_rank_from_axial_shift( + self.coord_2d, 1, -1, self.group_layout + ) + self.recv_rank_row = get_group_rank_from_axial_shift( + self.coord_2d, 1, 1, self.group_layout + ) + self.comm_row = One2OneComm( + self.group_2d, + self.send_rank_row, + self.recv_rank_row, + parity=ternary_parity(self.rank_2d, self.send_rank_row, self.recv_rank_row), + ) + + # Initial col shift: col j shifts up by j + self.send_rank_col_init = get_group_rank_from_axial_shift( + self.coord_2d, 0, -self.coord_2d[1], self.group_layout + ) + self.recv_rank_col_init = get_group_rank_from_axial_shift( + self.coord_2d, 0, self.coord_2d[1], self.group_layout + ) + self.comm_col_init = One2OneComm( + self.group_2d, + self.send_rank_col_init, + self.recv_rank_col_init, + parity=ternary_parity( + self.rank_2d, self.send_rank_col_init, self.recv_rank_col_init + ), + ) + + # Subsequent col shifts: up by 1 + self.send_rank_col = get_group_rank_from_axial_shift( + self.coord_2d, 0, -1, self.group_layout + ) + self.recv_rank_col = get_group_rank_from_axial_shift( + self.coord_2d, 0, 1, self.group_layout + ) + self.comm_col = One2OneComm( + self.group_2d, + self.send_rank_col, + self.recv_rank_col, + parity=ternary_parity(self.rank_2d, self.send_rank_col, self.recv_rank_col), + ) + + # Fused transpose + initial shift for backward pass + coords_t = self.coord_2d[::-1] + self.send_rank_transpose_row_init = get_group_rank_from_axial_shift( + coords_t, 1, -coords_t[0], self.group_layout + ) + recv_rank_transpose_row_init = get_group_rank_from_axial_shift( + self.coord_2d, 1, self.coord_2d[0], self.group_layout + ) + self.recv_rank_transpose_row_init = self.group_layout( + self.group_layout.unravel(recv_rank_transpose_row_init)[::-1] + ) + self.comm_transpose_row_init = One2OneComm( + self.group_2d, + self.send_rank_transpose_row_init, + self.recv_rank_transpose_row_init, + parity=ternary_parity( + self.rank_2d, + self.send_rank_transpose_row_init, + self.recv_rank_transpose_row_init, + ), + ) + + self.send_rank_transpose_col_init = get_group_rank_from_axial_shift( + coords_t, 0, -coords_t[1], self.group_layout + ) + recv_rank_transpose_col_init = get_group_rank_from_axial_shift( + self.coord_2d, 0, self.coord_2d[1], self.group_layout + ) + self.recv_rank_transpose_col_init = self.group_layout( + self.group_layout.unravel(recv_rank_transpose_col_init)[::-1] + ) + self.comm_transpose_col_init = One2OneComm( + self.group_2d, + self.send_rank_transpose_col_init, + self.recv_rank_transpose_col_init, + parity=ternary_parity( + self.rank_2d, + self.send_rank_transpose_col_init, + self.recv_rank_transpose_col_init, + ), + ) + + +class AttentionPairBiasComm: + """Communication setup for ring attention with pair bias. + + Manages transpose comms for K/V/mask and ring shift comms for K/V/Z. + """ + + def __init__( + self, + process_group: dist.ProcessGroup, + group_layout: LayoutMap, + cp_axis_0_group: dist.ProcessGroup, + cp_axis_1_group: dist.ProcessGroup, + ): + self.process_group = process_group + self.cp_axis_0_group = cp_axis_0_group + self.cp_axis_1_group = cp_axis_1_group + + if group_layout.shape is None: + raise ValueError("group_layout must have a shape") + self.world_size = dist.get_world_size(self.process_group) + if self.world_size != group_layout.numel: + raise ValueError("Inconsistent world_size and group_layout.numel") + if len(group_layout.shape) != 2: + raise ValueError(f"{self.__class__} only supports 2D group layout") + if group_layout.shape[0] != group_layout.shape[1]: + raise ValueError(f"group_layout.shape {group_layout.shape} is not square") + + self.group_layout = group_layout + self.global_rank = dist.get_rank() + self.group_rank = dist.get_rank(self.process_group) + self.rank_coords: tuple[int, ...] = self.group_layout.unravel(self.group_rank) + + self.comm_transpose_k = TransposeComm(self.process_group, self.group_layout) + self.comm_transpose_v = TransposeComm(self.process_group, self.group_layout) + self.comm_transpose_mask = TransposeComm(self.process_group, self.group_layout) + + self.send_rank_kvz = get_group_rank_from_axial_shift( + self.rank_coords, 1, 1, self.group_layout + ) + self.recv_rank_kvz = get_group_rank_from_axial_shift( + self.rank_coords, 1, -1, self.group_layout + ) + self.parity = self.rank_coords[1] % 2 == 1 + self.comm_k = One2OneComm( + self.process_group, + self.send_rank_kvz, + self.recv_rank_kvz, + parity=self.parity, + ) + self.comm_v = One2OneComm( + self.process_group, + self.send_rank_kvz, + self.recv_rank_kvz, + parity=self.parity, + ) + self.comm_z = One2OneComm( + self.process_group, + self.send_rank_kvz, + self.recv_rank_kvz, + parity=self.parity, + ) + + def __deepcopy__(self, memo): + return AttentionPairBiasComm( + self.process_group, + self.group_layout, + self.cp_axis_0_group, + self.cp_axis_1_group, + ) diff --git a/src/transformers/models/esmfold2/distributed/manager.py b/src/transformers/models/esmfold2/distributed/manager.py new file mode 100644 index 000000000000..f2b2c4b7e187 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/manager.py @@ -0,0 +1,576 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + + +import os +from copy import deepcopy +from math import prod +from typing import Any, Dict, Optional, OrderedDict, Union +from warnings import warn + +import torch + +from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( + LayoutMap, + LayoutRightMap, +) + +# grid_group_sizes objects must have +# (1) .values() attribute, (2) .items() attribute +_GridGroupSizesType = OrderedDict[str, Union[int, tuple[int, ...]]] + + +class DistributedManager: + """Borg-style singleton class for managing distributed state. + + Manages the device mesh, process groups, and subgroups for 2D context + parallelism. Initialized with e.g.:: + + DistributedManager.initialize(OrderedDict([("dp", 1), ("cp", (N, N))])) + + to create a 2D CP grid of size N×N. + """ + + # Borg-style shared state. Every instance's ``__dict__`` is rebound to + # ``_state`` in ``__new__``; one-time defaults are seeded via ``setdefault`` + # (rather than ``instance._foo = ...`` which pyright flags as "Cannot + # assign to attribute" on a class without those names declared). + _state: dict = {} + _DEFAULT_STATE: dict = { + "_initialized": False, + "_has_dist": False, + "_rank": 0, + "_world_size": 1, + "_local_rank": 0, + "_device": torch.device("cpu"), + "_backend": None, + "_device_mesh": None, + "_layout_device_mesh": None, + "_has_subgroups": False, + "_device_mesh_subgroups": None, + "_layout_device_mesh_subgroups": None, + "_group": {}, + "_group_rank": {}, + "_group_ranks": {}, + "_subgroups": {}, + "_subgroups_rank": {}, + "_subgroups_ranks": {}, + "_layout_subgroups": {}, + "_method_init": None, + } + + def __new__(cls): + instance = super().__new__(cls) + instance.__dict__ = cls._state + for key, default in cls._DEFAULT_STATE.items(): + cls._state.setdefault(key, default) + return instance + + @classmethod + def methods_init_available(cls) -> set[str]: + return {"ENV", "SLURM"} + + @classmethod + def backend_for_device(cls) -> Dict[str, Optional[str]]: + return { + "cuda": "nccl" if torch.distributed.is_nccl_available() else None, + "cpu": "gloo" if torch.distributed.is_gloo_available() else None, + } + + @classmethod + def is_initialized(cls) -> bool: + return cls._state.get("_initialized", False) + + def __init__(self): + if not self._initialized: + raise RuntimeError( + "A DistributedManager instance is being instantiated before " + "the singleton class is initialized. " + "Please call DistributedManager.initialize() first." + ) + super().__init__() + + def __getattr__(self, name: str) -> Any: + key_state = f"_{name}" + has_key_shared_state = key_state in self.__dict__ + has_key = name in self.__dict__ + if has_key_shared_state: + return self.__dict__[key_state] + elif has_key: + return self.__dict__[name] + else: + raise AttributeError(f'Attribute "{name}" or "_{name}" not found.') + + def __str__(self): + return ( + f"Initialized process {self.rank} of {self.world_size} using " + f"method '{self.method_init}'. Device set to {str(self.device)}. " + f"Backend is {self.backend}" + ) + + @staticmethod + def _setup( + grid_group_sizes: Optional[_GridGroupSizesType] = None, + device_type: str = "cuda", + backend: Optional[str] = None, + rank: int = -1, + node_rank: int = -1, + world_size: int = -1, + local_rank: Optional[int] = None, + addr: str = "localhost", + port: str = "29500", + method_init: str = "ENV", + **kwargs_init_pg, + ): + if device_type == "cuda" and not torch.cuda.is_available(): + raise RuntimeError( + f"Input device type {device_type} but torch.cuda is not available" + ) + + if world_size != -1 and grid_group_sizes is not None: + total_size = 1 + assert hasattr(grid_group_sizes, "values") + for value in grid_group_sizes.values(): + if isinstance(value, tuple) and all(isinstance(v, int) for v in value): + total_size *= prod(value) + elif isinstance(value, int): + total_size *= value + else: + raise RuntimeError( + f"Values in grid_group_sizes must be either int or tuple[int, ...], got {type(value)}" + ) + if world_size != total_size: + raise RuntimeError( + f"Non-default world_size {world_size} != product of grid_group_sizes values ({total_size})" + ) + + backend_for_device = DistributedManager.backend_for_device() + + if backend_for_device["cpu"] is None and backend_for_device["cuda"] is None: + raise RuntimeError( + f"No backend available for the supported device types: {backend_for_device.keys()}" + ) + + if device_type not in backend_for_device: + raise RuntimeError( + f"Invalid input device type {device_type}: only supports {backend_for_device.keys()}" + ) + + if backend is None: + backend = backend_for_device[device_type] + elif backend != backend_for_device[device_type]: + raise RuntimeError( + f"Invalid input backend {backend} for input device type {device_type}" + ) + + os.environ["MASTER_ADDR"] = addr + os.environ["MASTER_PORT"] = str(port) + + DistributedManager._state["_initialized"] = True + manager = DistributedManager() + + manager._has_dist = torch.distributed.is_available() # type: ignore[assignment] + manager._rank = rank # type: ignore[assignment] + manager._world_size = world_size # type: ignore[assignment] + manager._node_rank = node_rank # type: ignore[assignment] + + if device_type == "cuda": + if ( + manager.world_size > torch.cuda.device_count() + and manager.world_size % torch.cuda.device_count() + ): + warn("world_size is not a multiple of torch.cuda.device_count()") + if local_rank is None: + manager._local_rank = manager.rank % torch.cuda.device_count() # type: ignore[assignment] + else: + manager._local_rank = local_rank # type: ignore[assignment] + manager._device = torch.device(f"cuda:{manager.local_rank}") # type: ignore[assignment] + else: + if local_rank is not None: + manager._local_rank = local_rank # type: ignore[assignment] + manager._device = torch.device("cpu") # type: ignore[assignment] + + if not manager.has_dist: + warn("DistributedManager initialized without torch.distributed package") + return + + if manager.device.type == "cuda": + torch.cuda.set_device(manager.device) + torch.cuda.device(manager.device) + torch.cuda.empty_cache() + + manager._backend = backend # type: ignore[assignment] + + if manager.device.type == "cuda" and backend == "nccl": + try: + torch.distributed.init_process_group( + manager.backend, + rank=manager.rank, + world_size=manager.world_size, + device_id=manager.device, + **kwargs_init_pg, + ) + except TypeError: + torch.distributed.init_process_group( + manager.backend, + rank=manager.rank, + world_size=manager.world_size, + **kwargs_init_pg, + ) + else: + torch.distributed.init_process_group( + manager.backend, + rank=manager.rank, + world_size=manager.world_size, + **kwargs_init_pg, + ) + + manager._group["world"] = torch.distributed.group.WORLD + manager._group_rank["world"] = manager.rank + manager._group_ranks["world"] = torch.distributed.get_process_group_ranks( + manager.group["world"] + ) + manager._method_init = method_init # type: ignore[assignment] + + if grid_group_sizes is not None: + DistributedManager.create_grid_group(grid_group_sizes) + + @staticmethod + def _create_device_mesh_and_groups( + name: list[str], shape: list[int], suffix_mesh: Optional[str] = None + ) -> None: + if not DistributedManager.is_initialized(): + raise RuntimeError("DistributedManager is not initialized") + if ( + not DistributedManager._state["_has_dist"] + or not torch.distributed.is_available() + ): + raise RuntimeError( + "_create_device_mesh_and_groups requires torch.distributed" + ) + if ( + DistributedManager._state["_method_init"] is None + or DistributedManager._state["_method_init"] + not in DistributedManager.methods_init_available() + ): + raise RuntimeError( + f"Invalid DistributedManager method_init {DistributedManager._state['_method_init']}" + ) + if ( + DistributedManager._state["_backend"] is None + or DistributedManager._state["_backend"] + not in DistributedManager.backend_for_device().values() + ): + raise RuntimeError( + f"Invalid DistributedManager backend {DistributedManager._state['_backend']}" + ) + if ( + DistributedManager._state["_device"] is None + or DistributedManager._state["_device"].type + not in DistributedManager.backend_for_device().keys() + ): + raise RuntimeError( + f"Invalid DistributedManager device type {DistributedManager._state['_device'].type}" + ) + + world_size_expected = prod(shape) + if world_size_expected != DistributedManager._state["_world_size"]: + raise RuntimeError( + f"world_size {DistributedManager._state['_world_size']} does not match " + f"expected {world_size_expected} from shape {shape}" + ) + + device_type = DistributedManager._state["_device"].type + name_mesh = ( + f"_device_mesh_{suffix_mesh}" if suffix_mesh is not None else "_device_mesh" + ) + layout = LayoutRightMap(tuple(shape)) + DistributedManager._state[f"_layout{name_mesh}"] = layout + + grid2rank = torch.as_strided( + torch.arange(world_size_expected), size=layout.shape, stride=layout.strides + ) + DistributedManager._state[name_mesh] = torch.distributed.device_mesh.DeviceMesh( + device_type, grid2rank, mesh_dim_names=tuple(name) + ) + + for i_group in range(len(name)): + name_group = name[i_group] + if name_group in DistributedManager._state["_group"]: + continue + DistributedManager._state["_group"][name_group] = DistributedManager._state[ + name_mesh + ].get_group(name_group) + DistributedManager._state["_group_rank"][name_group] = ( + torch.distributed.get_group_rank( + DistributedManager._state["_group"][name_group], + DistributedManager._state["_rank"], + ) + ) + DistributedManager._state["_group_ranks"][name_group] = ( + torch.distributed.get_process_group_ranks( + DistributedManager._state["_group"][name_group] + ) + ) + + @staticmethod + def create_grid_group(grid_group_sizes: _GridGroupSizesType) -> None: + """Create a grid group for 2D context parallelism. + + Example:: + + from collections import OrderedDict + + DistributedManager.initialize(OrderedDict([("dp", 1), ("cp", (2, 2))])) + """ + shape_groups = [] + name_groups = [] + shape_subgroups = [] + name_subgroups = [] + group2subgroup = {} + group2subgroup_axes = {} + assert hasattr(grid_group_sizes, "items") + for k, v in grid_group_sizes.items(): + if isinstance(v, tuple) and all(isinstance(v_i, int) for v_i in v): + shape_groups.append(prod(v)) + name_groups.append(k) + shape_subgroups.extend(v) + names_this_subgroup = [f"{k}_axis_{i}" for i in range(len(v))] + name_subgroups.extend(names_this_subgroup) + group2subgroup[k] = names_this_subgroup + group2subgroup_axes[k] = list( + range(len(name_subgroups) - len(v), len(name_subgroups)) + ) + elif isinstance(v, int): + shape_groups.append(v) + name_groups.append(k) + shape_subgroups.append(v) + name_subgroups.append(k) + else: + raise RuntimeError( + f"Values in grid_group_sizes must be int or tuple[int, ...], got {type(v)}" + ) + + DistributedManager._create_device_mesh_and_groups(name_groups, shape_groups) + if (name_groups == name_subgroups) != (shape_groups == shape_subgroups): + raise RuntimeError("Inconsistent group and subgroup settings") + + DistributedManager._state["_has_subgroups"] = name_groups != name_subgroups + if DistributedManager._state["_has_subgroups"]: + if len(group2subgroup) == 0: + raise RuntimeError( + "group2subgroup is empty while _has_subgroups is True" + ) + DistributedManager._create_device_mesh_and_groups( + name_subgroups, shape_subgroups, suffix_mesh="subgroups" + ) + layout = DistributedManager._state["_layout_device_mesh_subgroups"] + coords = DistributedManager._state[ + "_device_mesh_subgroups" + ].get_coordinate() + for name_group, subgroup_names in group2subgroup.items(): + DistributedManager._state["_subgroups"][name_group] = [ + DistributedManager._state["_group"][n] for n in subgroup_names + ] + DistributedManager._state["_subgroups_ranks"][name_group] = [ + DistributedManager._state["_group_ranks"][n] for n in subgroup_names + ] + DistributedManager._state["_subgroups_rank"][name_group] = [ + DistributedManager._state["_group_rank"][n] for n in subgroup_names + ] + axes_subgroup = group2subgroup_axes[name_group] + slices = deepcopy(coords) + for axis in axes_subgroup: + slices[axis] = slice(None) + layout_subgroup = layout[tuple(slices)] + DistributedManager._state["_layout_subgroups"][name_group] = LayoutMap( + layout_subgroup.strides, layout_subgroup.shape, offset=0 + ) + + @staticmethod + def create_group(name: str, ranks: list[int], **kwargs_dist_ng) -> None: + DistributedManager._state["_group"][name] = torch.distributed.new_group( + ranks=ranks, **kwargs_dist_ng + ) + DistributedManager._state["_group_ranks"][name] = ranks + DistributedManager._state["_group_rank"][name] = ( + torch.distributed.get_group_rank( + DistributedManager._state["_group"][name], + DistributedManager._state["_rank"], + ) + ) + + @staticmethod + def _initialize_env(*args, **kwargs): + if not ("RANK" in os.environ and "WORLD_SIZE" in os.environ): + raise RuntimeError( + "environment variables RANK and WORLD_SIZE must be set for env:// initialization" + ) + rank = os.environ.get("RANK") + world_size = os.environ.get("WORLD_SIZE") + local_rank = os.environ.get("LOCAL_RANK") + group_rank = os.environ.get("GROUP_RANK", 0) + node_rank = int(os.environ.get("NODE_RANK", group_rank)) + try: + rank = int(rank) # type: ignore[arg-type] + world_size = int(world_size) # type: ignore[arg-type] + if local_rank is not None: + local_rank = int(local_rank) + except TypeError: + raise RuntimeError( + "environment variables RANK, LOCAL_RANK and WORLD_SIZE must be integers" + ) + DistributedManager._setup( + *args, + rank=rank, + node_rank=node_rank, + world_size=world_size, + local_rank=local_rank, + addr=os.environ.get("MASTER_ADDR"), # type: ignore[arg-type] + port=os.environ.get("MASTER_PORT"), # type: ignore[arg-type] + method_init="ENV", + **kwargs, + ) + + @staticmethod + def _initialize_slurm(*args, **kwargs): + keys = ( + "SLURM_PROCID", + "SLURM_NPROCS", + "SLURM_LOCALID", + "SLURM_LAUNCH_NODE_IPADDR", + ) + if not all(k in os.environ for k in keys): + raise RuntimeError( + f"environment variables {keys} must be set for SLURM initialization" + ) + rank = os.environ.get("SLURM_PROCID") + node_rank = int(os.environ.get("SLURM_NODEID", 0)) + world_size = os.environ.get("SLURM_NPROCS") + local_rank = os.environ.get("SLURM_LOCALID") + addr = os.environ.get("SLURM_LAUNCH_NODE_IPADDR") + try: + rank = int(rank) # type: ignore[arg-type] + world_size = int(world_size) # type: ignore[arg-type] + if local_rank is not None: + local_rank = int(local_rank) + except TypeError: + raise RuntimeError( + "environment variables SLURM_{PROCID,NPROCS,LOCALID} must be integers" + ) + DistributedManager._setup( + *args, + rank=rank, + node_rank=node_rank, + world_size=world_size, + local_rank=local_rank, + addr=addr, # type: ignore[arg-type] + method_init="SLURM", + **kwargs, + ) + + @staticmethod + def initialize( + grid_group_sizes: Optional[OrderedDict[str, int | tuple[int, ...]]] = None, + device_type: str = "cuda", + backend: Optional[str] = None, + **kwargs_init_pg, + ): + """Initialize the DistributedManager singleton. + + Parameters + ---------- + grid_group_sizes: + E.g. ``OrderedDict([("dp", 1), ("cp", (2, 2))])`` for a 2×2 CP grid. + device_type: + "cuda" (default) or "cpu". + backend: + Defaults to nccl for cuda, gloo for cpu. + """ + if DistributedManager.is_initialized(): + warn("DistributedManager is already initialized. Skip initialize()") + return + if backend == "nccl": + os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "0" + method_init = os.getenv("ESMCFOLD_DISTRIBUTED_INIT_METHOD") + if ( + method_init is not None + and method_init not in DistributedManager.methods_init_available() + ): + raise ValueError( + f"Unknown ESMCFOLD_DISTRIBUTED_INIT_METHOD={method_init}. " + f"Allowed: {DistributedManager.methods_init_available()}" + ) + if method_init is None: + try: + DistributedManager._initialize_env( + grid_group_sizes, + device_type=device_type, + backend=backend, + **kwargs_init_pg, + ) + except RuntimeError as except_env: + try: + DistributedManager._initialize_slurm( + grid_group_sizes, + device_type=device_type, + backend=backend, + **kwargs_init_pg, + ) + except RuntimeError as except_slurm: + warn( + "Could not initialize DistributedManager with env:// nor slurm.\n" + f"Error env://: {except_env}\n" + f"Error slurm: {except_slurm}\n" + "Will default initialize DistributedManager" + ) + DistributedManager._state["_initialized"] = True + elif method_init == "ENV": + DistributedManager._initialize_env( + grid_group_sizes, + device_type=device_type, + backend=backend, + **kwargs_init_pg, + ) + elif method_init == "SLURM": + DistributedManager._initialize_slurm( + grid_group_sizes, + device_type=device_type, + backend=backend, + **kwargs_init_pg, + ) + + @staticmethod + def cleanup(): + if DistributedManager._state.get("_group", {}) != {}: + if torch.distributed.is_initialized(): + if ( + DistributedManager._state["_device"].type == "cuda" + and torch.cuda.is_available() + ): + torch.distributed.barrier( + device_ids=[DistributedManager._state["_local_rank"]] + ) + else: + torch.distributed.barrier() + torch.distributed.destroy_process_group() + else: + DistributedManager._state = {} diff --git a/src/transformers/models/esmfold2/distributed/model/__init__.py b/src/transformers/models/esmfold2/distributed/model/__init__.py new file mode 100644 index 000000000000..eda8d5e6986f --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/model/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT diff --git a/src/transformers/models/esmfold2/distributed/model/layers/__init__.py b/src/transformers/models/esmfold2/distributed/model/layers/__init__.py new file mode 100644 index 000000000000..eda8d5e6986f --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/model/layers/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT diff --git a/src/transformers/models/esmfold2/distributed/model/layers/layernorm.py b/src/transformers/models/esmfold2/distributed/model/layers/layernorm.py new file mode 100644 index 000000000000..88fec07fa026 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/model/layers/layernorm.py @@ -0,0 +1,205 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Distributed LayerNorm with parameters replicated across the device mesh.""" + +from typing import Optional, Union + +import torch +import torch.distributed as dist +import torch.nn as nn +import torch.nn.functional as F +from torch.distributed.tensor import DeviceMesh, DTensor, Replicate, distribute_tensor + +_shape_t = Union[int, list[int], torch.Size] + + +class _LayerNormParamsReplicatedImpl(torch.autograd.Function): + """LayerNorm with replicated parameters and arbitrary DTensor input placement.""" + + @staticmethod + def forward( + ctx, + x: DTensor, + normalized_shape: list[int], + weight: Optional[DTensor], + bias: Optional[DTensor], + eps: float, + reduce_group: dist.ProcessGroup, + ) -> DTensor: + if not isinstance(x, DTensor): + raise TypeError(f"x must be DTensor, got {type(x)}") + + x_local = x.to_local() + weight_local = weight.to_local() if weight is not None else None + bias_local = bias.to_local() if bias is not None else None + + ctx.reduce_group = reduce_group + ctx.eps = eps + ctx.normalized_shape = normalized_shape + ctx.x_shape = x.shape + ctx.x_stride = x.stride() + ctx.x_placements = x.placements + ctx.device_mesh = x.device_mesh + ctx.save_for_backward(x_local, weight_local) + + out_local = F.layer_norm( + x_local, normalized_shape, weight_local, bias_local, eps + ) + return DTensor.from_local( + out_local, + device_mesh=x.device_mesh, + placements=x.placements, + shape=x.shape, + stride=x.stride(), + ) + + @staticmethod + def backward(ctx, d_out: DTensor): + if not isinstance(d_out, DTensor): + raise TypeError(f"d_out must be DTensor, got {type(d_out)}") + + x_saved, weight_saved = ctx.saved_tensors + d_out_local = d_out.to_local() + eps = ctx.eps + normalized_shape = ctx.normalized_shape + + dx_dtensor: Optional[DTensor] = None + dw_dtensor: Optional[DTensor] = None + db_dtensor: Optional[DTensor] = None + + if ctx.needs_input_grad[0] or ctx.needs_input_grad[2]: + dims = tuple(-(i + 1) for i in range(len(normalized_shape))) + mean = x_saved.mean(dim=dims, keepdim=True) + var = x_saved.var(dim=dims, unbiased=False, keepdim=True) + x_norm = (x_saved - mean) / torch.sqrt(var + eps) + + if ctx.needs_input_grad[0]: + if weight_saved is not None: + dy = d_out_local * weight_saved.view( + *([1] * (d_out_local.ndim - len(normalized_shape))), + *weight_saved.shape, + ) + else: + dy = d_out_local + dims = tuple(-(i + 1) for i in range(len(normalized_shape))) + dy_mean = dy.mean(dim=dims, keepdim=True) + dy_x_norm_mean = (dy * x_norm).mean(dim=dims, keepdim=True) + dx_local = (dy - dy_mean - x_norm * dy_x_norm_mean) / torch.sqrt(var + eps) + dx_dtensor = DTensor.from_local( + dx_local, + device_mesh=ctx.device_mesh, + placements=ctx.x_placements, + shape=ctx.x_shape, + stride=ctx.x_stride, + ) + + if ctx.needs_input_grad[2]: + reduce_dims = list(range(d_out_local.ndim - len(normalized_shape))) + dw = (d_out_local * x_norm).sum(dim=reduce_dims).contiguous() + dw_work = dist.all_reduce( + dw, op=dist.ReduceOp.SUM, group=ctx.reduce_group, async_op=True + ) + + if ctx.needs_input_grad[3]: + reduce_dims = list(range(d_out_local.ndim - len(normalized_shape))) + db = d_out_local.sum(dim=reduce_dims).contiguous() + db_work = dist.all_reduce( + db, op=dist.ReduceOp.SUM, group=ctx.reduce_group, async_op=True + ) + + replicate = [Replicate()] * ctx.device_mesh.ndim + if ctx.needs_input_grad[2]: + dw_work.wait() # type: ignore[union-attr] + dw_dtensor = DTensor.from_local( + dw, + device_mesh=ctx.device_mesh, + placements=replicate, + shape=dw.shape, + stride=dw.stride(), + ) + if ctx.needs_input_grad[3]: + db_work.wait() # type: ignore[union-attr] + db_dtensor = DTensor.from_local( + db, + device_mesh=ctx.device_mesh, + placements=replicate, + shape=db.shape, + stride=db.stride(), + ) + + return dx_dtensor, None, dw_dtensor, db_dtensor, None, None + + +class LayerNormParamsReplicated(nn.Module): + """nn.LayerNorm wrapper with parameters replicated over the device mesh. + + Accepts DTensor inputs with arbitrary placements and outputs DTensors + with the same placements. + + Parameters + ---------- + layer_local: + The serial nn.LayerNorm layer whose parameters to replicate. + device_mesh: + The device mesh for distributing tensors. + """ + + def __init__(self, layer_local: nn.LayerNorm, device_mesh: DeviceMesh) -> None: + super().__init__() + if not isinstance(layer_local, nn.LayerNorm): + raise TypeError( + f"layer_local must be nn.LayerNorm, got {type(layer_local)}" + ) + + self.device_mesh = device_mesh + self.normalized_shape = list(layer_local.normalized_shape) + self.eps = layer_local.eps + replicate_placements = [Replicate()] * device_mesh.ndim + + if layer_local.weight is not None: + self.weight = nn.Parameter( + distribute_tensor(layer_local.weight, device_mesh, replicate_placements) + ) + else: + self.weight = None + + if layer_local.bias is not None: + self.bias = nn.Parameter( + distribute_tensor(layer_local.bias, device_mesh, replicate_placements) + ) + else: + self.bias = None + + if "cp" in device_mesh.mesh_dim_names: # type: ignore[operator] + self._reduce_group = device_mesh.get_group("cp") + else: + self._reduce_group = dist.group.WORLD + + def forward(self, x: DTensor) -> DTensor: + return _LayerNormParamsReplicatedImpl.apply( # type: ignore[return-value] + x, + self.normalized_shape, + self.weight, + self.bias, + self.eps, + self._reduce_group, + ) diff --git a/src/transformers/models/esmfold2/distributed/model/layers/linear.py b/src/transformers/models/esmfold2/distributed/model/layers/linear.py new file mode 100644 index 000000000000..ba195f0509a9 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/model/layers/linear.py @@ -0,0 +1,199 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Distributed linear layer with parameters replicated across the device mesh.""" + +from typing import Optional + +import torch +import torch.distributed as dist +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor, Replicate, distribute_tensor + +from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( + update_exhaustive_strides, +) + + +class _LinearParamsReplicatedImpl(torch.autograd.Function): + """Linear layer with replicated parameters and arbitrary input placements. + + Parameters are replicated on all mesh dimensions; inputs can be sharded + (e.g. Shard(0), Shard(1), Shard(2)) or replicated. Backward uses + all-reduce over the cp group to accumulate parameter gradients. + """ + + @staticmethod + def forward( + ctx, + x: DTensor, + weight: DTensor, + bias: Optional[DTensor], + reduce_group: dist.ProcessGroup, + avg_reduce: bool, + ) -> DTensor: + if not isinstance(x, DTensor): + raise TypeError(f"x must be DTensor, got {type(x)}") + if not isinstance(weight, DTensor): + raise TypeError(f"weight must be DTensor, got {type(weight)}") + + ctx.reduce_group = reduce_group + ctx.avg_reduce = avg_reduce + + x_local = x.to_local() + weight_local = weight.to_local() + bias_local = bias.to_local() if bias is not None else None + + ctx.save_for_backward(x_local, weight_local) + ctx.x_shape = x.shape + ctx.x_stride = x.stride() + ctx.x_placements = x.placements + ctx.device_mesh = x.device_mesh + + out_local = F.linear(x_local, weight_local, bias_local) + + shape_output = x.shape[:-1] + (weight.shape[0],) + stride_output = update_exhaustive_strides(x.shape, x.stride(), shape_output) + return DTensor.from_local( + out_local, + device_mesh=x.device_mesh, + placements=x.placements, + shape=shape_output, + stride=stride_output, + ) + + @staticmethod + def backward(ctx, d_out: DTensor): + if not isinstance(d_out, DTensor): + raise TypeError(f"d_out must be DTensor, got {type(d_out)}") + + x_saved, weight_saved = ctx.saved_tensors + d_out_local = d_out.to_local() + + dw: Optional[Tensor] = None + dw_work = None + if ctx.needs_input_grad[1]: + # Aggregate over all but the last two dims (batch + seq dims) + dw = torch.einsum("...i,...j->ij", d_out_local, x_saved) + dw = dw.contiguous() # type: ignore[union-attr] + op = dist.ReduceOp.AVG if ctx.avg_reduce else dist.ReduceOp.SUM + dw_work = dist.all_reduce(dw, op=op, group=ctx.reduce_group, async_op=True) + + db: Optional[Tensor] = None + db_work = None + if ctx.needs_input_grad[2]: + reduce_dims = list(range(d_out_local.ndim - 1)) + db = d_out_local.sum(dim=reduce_dims).contiguous() + op = dist.ReduceOp.AVG if ctx.avg_reduce else dist.ReduceOp.SUM + db_work = dist.all_reduce(db, op=op, group=ctx.reduce_group, async_op=True) + + dx_dtensor: Optional[DTensor] = None + if ctx.needs_input_grad[0]: + dx_local = F.linear(d_out_local, weight_saved.t()) + shape_dx = ctx.x_shape + stride_dx = ctx.x_stride + dx_dtensor = DTensor.from_local( + dx_local, + device_mesh=ctx.device_mesh, + placements=ctx.x_placements, + shape=shape_dx, + stride=stride_dx, + ) + + # Wrap parameter gradients as DTensors with Replicate placement + replicate = [Replicate()] * ctx.device_mesh.ndim + dw_dtensor: Optional[DTensor] = None + if dw_work is not None: + dw_work.wait() + dw_dtensor = DTensor.from_local( + dw, # type: ignore[arg-type] + device_mesh=ctx.device_mesh, + placements=replicate, + shape=dw.shape, # type: ignore[union-attr] + stride=dw.stride(), # type: ignore[union-attr] + ) + + db_dtensor: Optional[DTensor] = None + if db_work is not None: + db_work.wait() + db_dtensor = DTensor.from_local( + db, # type: ignore[arg-type] + device_mesh=ctx.device_mesh, + placements=replicate, + shape=db.shape, # type: ignore[union-attr] + stride=db.stride(), # type: ignore[union-attr] + ) + + return dx_dtensor, dw_dtensor, db_dtensor, None, None + + +class LinearParamsReplicated(nn.Module): + """nn.Linear wrapper with parameters replicated over the device mesh. + + Accepts DTensor inputs with arbitrary placements and outputs DTensors + with the same placements. Parameter gradients are all-reduced across + the CP group so every rank accumulates the full gradient. + + Parameters + ---------- + layer_local: + The serial nn.Linear layer whose parameters to replicate. + device_mesh: + The device mesh for distributing tensors. + avg_reduce: + If True, use AVG instead of SUM for all-reduce (useful when the + effective batch is already averaged). + """ + + def __init__( + self, layer_local: nn.Linear, device_mesh: DeviceMesh, avg_reduce: bool = False + ) -> None: + super().__init__() + if not isinstance(layer_local, nn.Linear): + raise TypeError(f"layer_local must be nn.Linear, got {type(layer_local)}") + + self.device_mesh = device_mesh + self.avg_reduce = avg_reduce + replicate_placements = [Replicate()] * device_mesh.ndim + + self.weight = nn.Parameter( + distribute_tensor(layer_local.weight, device_mesh, replicate_placements) + ) + if layer_local.bias is not None: + self.bias = nn.Parameter( + distribute_tensor(layer_local.bias, device_mesh, replicate_placements) + ) + else: + self.bias = None + + # Choose reduce group: use cp group if present, otherwise world + if "cp" in device_mesh.mesh_dim_names: # type: ignore[operator] + self._reduce_group = device_mesh.get_group("cp") + else: + self._reduce_group = dist.group.WORLD + + def forward(self, x: DTensor) -> DTensor: + return _LinearParamsReplicatedImpl.apply( # type: ignore[return-value] + x, self.weight, self.bias, self._reduce_group, self.avg_reduce + ) diff --git a/src/transformers/models/esmfold2/distributed/model/layers/pairformer.py b/src/transformers/models/esmfold2/distributed/model/layers/pairformer.py new file mode 100644 index 000000000000..038f1471b1f2 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/model/layers/pairformer.py @@ -0,0 +1,181 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Distributed PairUpdateBlock and Pairformer for ESMFold2. + +ESMFold2's Pairformer is pair-only (no single/sequence track), with each block: + PairUpdateBlock: + pair = pair + tri_mul_out(pair) + pair = pair + tri_mul_in(pair) + pair = pair_transition(pair) + +All three operations are distributed across the 2D CP grid. +""" + +from typing import Optional + +import torch.nn as nn +import torch.nn.functional as F +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor + +from projects.huggingface.transformers.models.esmfold2.distributed.comm import ( + Ring2DComm, +) +from projects.huggingface.transformers.models.esmfold2.distributed.manager import ( + DistributedManager, +) +from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.layernorm import ( + LayerNormParamsReplicated, +) +from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.linear import ( + LinearParamsReplicated, +) +from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.triangular_mult import ( + TriangleMultiplicativeBlockDistributed, +) +from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( + FoldingTrunk as SerialFoldingTrunk, +) +from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( + PairUpdateBlock as SerialPairUpdateBlock, +) +from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( + Transition as SerialTransition, +) + + +class TransitionDistributed(nn.Module): + """Distributed Transition block (LayerNorm + SwiGLU FFN). + + Parameters are replicated; the LayerNorm and both Linear layers + use DTensor with replicated params. + """ + + def __init__(self, layer: SerialTransition, device_mesh: DeviceMesh) -> None: + super().__init__() + if not isinstance(layer, SerialTransition): + raise TypeError(f"layer must be Transition, got {type(layer).__name__}") + self.norm = LayerNormParamsReplicated(layer.norm, device_mesh) + # SwiGLUMLP has w12 and w3 + self.w12 = LinearParamsReplicated(layer.ffn.w12, device_mesh) + self.w3 = LinearParamsReplicated(layer.ffn.w3, device_mesh) + self.hidden_features = layer.ffn.hidden_features + + def forward(self, x: DTensor) -> DTensor: + normed = self.norm(x) + x12 = self.w12(normed) + x1, x2 = x12.split(self.hidden_features, dim=-1) + hidden = F.silu(x1) * x2 + out = self.w3(hidden) + return x + out + + +class PairUpdateBlockDistributed(nn.Module): + """Distributed PairUpdateBlock. + + Computes: + pair = pair + tri_mul_out(pair, mask) + pair = pair + tri_mul_in(pair, mask) + pair = pair_transition(pair) + + All pair operations are distributed via 2D CP ring communication. + + Parameters + ---------- + layer: + Serial PairUpdateBlock to distribute. + dist_manager: + DistributedManager with the CP group and subgroups set up. + """ + + def __init__( + self, layer: SerialPairUpdateBlock, dist_manager: DistributedManager + ) -> None: + super().__init__() + if not isinstance(layer, SerialPairUpdateBlock): + raise TypeError( + f"layer must be PairUpdateBlock, got {type(layer).__name__}" + ) + + self.dist_manager = dist_manager + self.device_mesh = dist_manager.device_mesh_subgroups + + ring_comm_out = Ring2DComm( + dist_manager.group["cp"], + dist_manager.subgroups["cp"][0], + dist_manager.layout_subgroups["cp"], + ) + ring_comm_in = Ring2DComm( + dist_manager.group["cp"], + dist_manager.subgroups["cp"][0], + dist_manager.layout_subgroups["cp"], + ) + + self.tri_mul_out = TriangleMultiplicativeBlockDistributed( + layer.tri_mul_out._engine, self.device_mesh, ring_comm_out + ) + self.tri_mul_in = TriangleMultiplicativeBlockDistributed( + layer.tri_mul_in._engine, self.device_mesh, ring_comm_in + ) + self.pair_transition = TransitionDistributed( + layer.pair_transition, self.device_mesh + ) + + def forward( + self, pair: DTensor, pair_attention_mask: Optional[DTensor] = None + ) -> DTensor: + pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) + pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) + pair = self.pair_transition(pair) + return pair + + +class FoldingTrunkDistributed(nn.Module): + """Distributed Pairformer: ModuleList of PairUpdateBlockDistributed. + + Wraps the serial Pairformer by distributing each block. + + Parameters + ---------- + pairformer: + Serial Pairformer module. + dist_manager: + DistributedManager with the CP group and subgroups set up. + """ + + def __init__( + self, trunk: SerialFoldingTrunk, dist_manager: DistributedManager + ) -> None: + super().__init__() + if not isinstance(trunk, SerialFoldingTrunk): + raise TypeError(f"trunk must be FoldingTrunk, got {type(trunk).__name__}") + + self.blocks = nn.ModuleList( + [PairUpdateBlockDistributed(block, dist_manager) for block in trunk.blocks] # type: ignore[arg-type] + ) + + def forward( + self, pair: DTensor, pair_attention_mask: Optional[DTensor] = None + ) -> DTensor: + for block in self.blocks: + pair = block(pair, pair_attention_mask=pair_attention_mask) + return pair diff --git a/src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py b/src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py new file mode 100644 index 000000000000..da22c40b2710 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py @@ -0,0 +1,427 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Distributed TriangleMultiplicativeBlock for ESMFold2's pair representation. + +The pair tensor z has shape (B, N, N, d_pair) and is distributed across a 2D +CP grid as DTensor with placements (Shard(0), Shard(1), Shard(2)). GPU (i, j) +owns the shard z[..., i_start:i_end, j_start:j_end, :]. + +Triangle multiplication patterns: + Outgoing: contracted[b,n,m,d] = sum_k a[b,n,k,d] * b[b,m,k,d] (einsum "bnkd,bmkd->bnmd") + Incoming: contracted[b,n,m,d] = sum_k a[b,k,n,d] * b[b,k,m,d] (einsum "bknd,bkmd->bnmd") + +The distributed BMM uses ring communication to accumulate partial results: + - One operand is transposed across the 2D grid (so (i,j) gets the chunk from (j,i)) + - Both operands are ring-shifted (row-wise and column-wise respectively) + - Each step computes a local matmul; results are accumulated +""" + +from enum import Enum, auto +from typing import Tuple + +import torch +from torch import nn +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor, Shard + +from projects.huggingface.transformers.models.esmfold2.distributed.comm import ( + Ring2DComm, +) +from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.layernorm import ( + LayerNormParamsReplicated, +) +from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.linear import ( + LinearParamsReplicated, +) +from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( + update_exhaustive_strides, +) +from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( + TriangleMultiplicativeBlock as SerialTriangleMultiplicativeBlock, +) + + +class _Direction(Enum): + Outgoing = auto() + Incoming = auto() + + +# --------------------------------------------------------------------------- +# Core distributed batch matmul +# --------------------------------------------------------------------------- + + +class _XposeArgs(Enum): + lhs = auto() + rhs = auto() + + +def _distributed_bmm( + lhs: torch.Tensor, + rhs: torch.Tensor, + comm: Ring2DComm, + permute_lhs: tuple[int, ...] | None = None, + permute_rhs: tuple[int, ...] | None = None, + permute_out: tuple[int, ...] | None = None, + xpose_args: _XposeArgs | None = None, +) -> torch.Tensor: + """Distributed batch matmul using ring communication on a 2D process grid. + + See boltz-cp's comm.py diagrams for the full algorithm description. + + Parameters + ---------- + lhs, rhs: + Local tensor shards. Shape: (B, ..., N, K) after optional permute. + comm: + Ring2DComm object providing all the communication handles. + permute_lhs, permute_rhs: + Optional permutations applied before computation. + permute_out: + Optional permutation applied to the output. + xpose_args: + Which operand to transpose across the grid first. + """ + if permute_lhs is not None: + lhs = lhs.permute(permute_lhs) + lhs = lhs.clone(memory_format=torch.contiguous_format) + if permute_rhs is not None: + rhs = rhs.permute(permute_rhs) + rhs = rhs.clone(memory_format=torch.contiguous_format) + + if xpose_args == _XposeArgs.lhs: + lhs_recv = comm.comm_2d_trans.enqueue_to_dispatch(lhs) + rhs_recv = rhs + rhs = torch.empty_like(rhs_recv) + elif xpose_args == _XposeArgs.rhs: + rhs_recv = comm.comm_2d_trans.enqueue_to_dispatch(rhs) + lhs_recv = lhs + lhs = torch.empty_like(lhs_recv) + elif xpose_args is None: + lhs_recv = lhs + lhs = torch.empty_like(lhs_recv) + rhs_recv = rhs + rhs = torch.empty_like(rhs_recv) + else: + raise ValueError(f"Invalid xpose_args: {xpose_args}") + + i_ready = 0 + i_recv = i_ready ^ 1 + lhs_buffer = [lhs_recv, lhs] + rhs_buffer = [rhs_recv, rhs] + + if xpose_args is not None: + comm.comm_2d_trans.wait_until_finished() + + lhs_buffer[i_recv] = comm.comm_row_init.enqueue_to_dispatch( + lhs_buffer[i_ready], lhs_buffer[i_recv] + ) + rhs_buffer[i_recv] = comm.comm_col_init.enqueue_to_dispatch( + rhs_buffer[i_ready], rhs_buffer[i_recv] + ) + + i_ready ^= 1 + i_recv ^= 1 + + out = torch.zeros_like(lhs_buffer[i_ready]) + + comm.comm_row_init.wait_until_finished() + comm.comm_col_init.wait_until_finished() + + for k_step in range(comm.group_layout.shape[1]): + lhs_ready = lhs_buffer[i_ready] + rhs_ready = rhs_buffer[i_ready] + if k_step < comm.group_layout.shape[1] - 1: + lhs_buffer[i_recv] = comm.comm_row.enqueue_to_dispatch( + lhs_ready, lhs_buffer[i_recv] + ) + rhs_buffer[i_recv] = comm.comm_col.enqueue_to_dispatch( + rhs_ready, rhs_buffer[i_recv] + ) + out = out + torch.matmul(lhs_ready, rhs_ready) + if k_step < comm.group_layout.shape[1] - 1: + comm.comm_row.wait_until_finished() + comm.comm_col.wait_until_finished() + i_ready ^= 1 + i_recv ^= 1 + + if permute_out is not None: + out = out.permute(permute_out) + return out + + +# --------------------------------------------------------------------------- +# Autograd function for distributed triangle multiplication +# --------------------------------------------------------------------------- + + +class _TriangleMultiplicativeBlockImpl(torch.autograd.Function): + """Distributed triangle multiplication autograd function. + + Forward + ------- + Input x has shape (B, N_local_row, N_local_col, 2*d) and is already: + x = signal * sigmoid(gate_logits) * visibility_mask (pre-combined inner gate + mask) + + The function splits x into a = x[..., :d] and b = x[..., d:], then computes + the triangle multiplication using ring communication. + + Backward + -------- + Propagates gradients back through the distributed BMM. + """ + + @staticmethod + @torch.amp.custom_fwd(device_type="cuda") + def forward(ctx, x: DTensor, comm: Ring2DComm, direction: _Direction) -> DTensor: + if not isinstance(x, DTensor): + raise TypeError(f"x must be DTensor, got {type(x)}") + if x.ndim != 4: + raise ValueError(f"x must be 4D, got {x.ndim}D") + if x.shape[-1] % 2 != 0: + raise ValueError(f"Last dim of x must be even, got {x.shape[-1]}") + placements = x.placements + if placements != (Shard(0), Shard(1), Shard(2)): + raise ValueError( + f"x must have placements (Shard(0), Shard(1), Shard(2)), got {placements}" + ) + + x_local = x.to_local() + a_local, b_local = torch.chunk(x_local, 2, dim=-1) + a_local = a_local.clone(memory_format=torch.contiguous_format) + b_local = b_local.clone(memory_format=torch.contiguous_format) + + if x.requires_grad: + ctx.save_for_backward(a_local, b_local) + ctx.comm = comm + ctx.shape_x = x.shape + ctx.stride_x = x.stride() + ctx.placements = placements + ctx.device_mesh = x.device_mesh + ctx.direction = direction + + if direction == _Direction.Outgoing: + # contracted[b,n,m,d] = sum_k a[b,n,k,d] * b[b,m,k,d] + # a: (B,n,k,d) → permute to (B,D,n,k) + # b: (B,m,k,d) → permute to (B,D,k,m) (needs transpose of (B,D,n,k)) + permute_lhs = (0, 3, 1, 2) + permute_rhs = (0, 3, 2, 1) + permute_out = (0, 2, 3, 1) + xpose_args = _XposeArgs.rhs + elif direction == _Direction.Incoming: + # contracted[b,n,m,d] = sum_k a[b,k,n,d] * b[b,k,m,d] + # a: (B,k,n,d) → permute to (B,D,n,k) => need (0,3,2,1) + # b: (B,k,m,d) → permute to (B,D,k,m) => need (0,3,1,2) + permute_lhs = (0, 3, 2, 1) + permute_rhs = (0, 3, 1, 2) + permute_out = (0, 2, 3, 1) + xpose_args = _XposeArgs.lhs + else: + raise ValueError(f"Invalid direction: {direction}") + + out_local = _distributed_bmm( + a_local, + b_local, + comm, + permute_lhs=permute_lhs, + permute_rhs=permute_rhs, + permute_out=permute_out, + xpose_args=xpose_args, + ).contiguous() + + # Output has shape (B, N_local_row, N_local_col, d) + shape_output = x.shape[:-1] + (out_local.shape[-1],) + stride_output = update_exhaustive_strides(x.shape, x.stride(), shape_output) + return DTensor.from_local( + out_local, + device_mesh=x.device_mesh, + placements=placements, + shape=shape_output, + stride=stride_output, + ) + + @staticmethod + @torch.amp.custom_bwd(device_type="cuda") + def backward(ctx, d_out: DTensor) -> Tuple[DTensor, None, None]: + if not isinstance(d_out, DTensor): + raise TypeError(f"d_out must be DTensor, got {type(d_out)}") + + a, b = ctx.saved_tensors + comm = ctx.comm + direction = ctx.direction + d_out_local = d_out.to_local().to(dtype=a.dtype) + + if direction == _Direction.Outgoing: + # d_a: d_out[b,n,m,d] * b[b,m,k,d] -> d_a[b,n,k,d] + # permute: d_out (B,n,m,D)->(B,D,n,m); b (B,m,k,D)->(B,D,m,k); out (B,D,n,k)->(B,n,k,D) + lhs_da, rhs_da = d_out_local, b + permute_lhs_da = (0, 3, 1, 2) + permute_rhs_da = (0, 3, 1, 2) + permute_out_da = (0, 2, 3, 1) + xpose_da = None + + # d_b: d_out[b,n,m,d] * a[b,n,k,d] -> d_b[b,m,k,d] + # permute: d_out (B,n,m,D)->(B,D,m,n); a (B,n,k,D)->(B,D,n,k); out (B,D,m,k)->(B,m,k,D) + lhs_db, rhs_db = d_out_local, a + permute_lhs_db = (0, 3, 2, 1) + permute_rhs_db = (0, 3, 1, 2) + permute_out_db = (0, 2, 3, 1) + xpose_db = _XposeArgs.lhs + + elif direction == _Direction.Incoming: + # d_a: d_out[b,n,m,d] * b[b,k,m,d] -> d_a[b,k,n,d] + lhs_da, rhs_da = b, d_out_local + permute_lhs_da = (0, 3, 1, 2) + permute_rhs_da = (0, 3, 2, 1) + permute_out_da = (0, 2, 3, 1) + xpose_da = _XposeArgs.rhs + + # d_b: d_out[b,n,m,d] * a[b,k,n,d] -> d_b[b,k,m,d] + lhs_db, rhs_db = a, d_out_local + permute_lhs_db = (0, 3, 1, 2) + permute_rhs_db = (0, 3, 1, 2) + permute_out_db = (0, 2, 3, 1) + xpose_db = None + else: + raise ValueError(f"Invalid direction: {direction}") + + da_local = _distributed_bmm( + lhs_da, + rhs_da, + comm, + permute_lhs=permute_lhs_da, + permute_rhs=permute_rhs_da, + permute_out=permute_out_da, + xpose_args=xpose_da, + ).contiguous() + db_local = _distributed_bmm( + lhs_db, + rhs_db, + comm, + permute_lhs=permute_lhs_db, + permute_rhs=permute_rhs_db, + permute_out=permute_out_db, + xpose_args=xpose_db, + ).contiguous() + + dab_local = torch.cat([da_local, db_local], dim=-1) + dx = DTensor.from_local( + dab_local, + device_mesh=ctx.device_mesh, + placements=ctx.placements, + shape=ctx.shape_x, + stride=ctx.stride_x, + ) + return dx, None, None + + +# --------------------------------------------------------------------------- +# Public distributed module +# --------------------------------------------------------------------------- + + +class TriangleMultiplicativeBlockDistributed(nn.Module): + """Distributed TriangleMultiplicativeBlock for ESMFold2's pair representation. + + Replaces the serial layer in a model by: + 1. Replacing all parameters with DTensor replicated parameters. + 2. Implementing the forward pass using distributed ring-communication BMM. + + The pair tensor z is expected as a DTensor with placements + (Shard(0), Shard(1), Shard(2)) on a 3D mesh (dp, cp_axis_0, cp_axis_1). + + Parameters + ---------- + layer: + The serial TriangleMultiplicativeBlock to distribute. + device_mesh: + The device mesh (should be the subgroups mesh: dp × cp_axis_0 × cp_axis_1). + comm: + Ring2DComm for the CP group. + """ + + def __init__( + self, + layer: SerialTriangleMultiplicativeBlock, + device_mesh: DeviceMesh, + comm: Ring2DComm, + ) -> None: + super().__init__() + if not isinstance(layer, SerialTriangleMultiplicativeBlock): + raise TypeError( + f"layer must be TriangleMultiplicativeBlock, got {type(layer).__name__}" + ) + self.device_mesh = device_mesh + self.ring_comm = comm + + self._direction = ( + _Direction.Outgoing if layer.flow == "outgoing" else _Direction.Incoming + ) + self._latent_channels = layer.latent_channels + + self.norm_start = LayerNormParamsReplicated(layer.norm_start, device_mesh) + self.norm_mix = LayerNormParamsReplicated(layer.norm_mix, device_mesh) + self.proj_bundle = LinearParamsReplicated(layer.proj_bundle, device_mesh) + self.proj_emit = LinearParamsReplicated(layer.proj_emit, device_mesh) + self.proj_gate = LinearParamsReplicated(layer.proj_gate, device_mesh) + + def forward(self, pair: DTensor, mask: DTensor | None = None) -> DTensor: + """Forward pass. + + Parameters + ---------- + pair: + Pair tensor (B, N, N, d_pair) as DTensor(Shard(0), Shard(1), Shard(2)). + mask: + Visibility mask (B, N, N) as DTensor(Shard(0), Shard(1), Shard(2)). + If None, no masking is applied. + + Returns + ------- + DTensor of same shape and placements as pair. + """ + # 1. Layer-normalise input + normalized = self.norm_start(pair) + + # 2. Compute bundled projection: (d → 4*d) + bundled = self.proj_bundle(normalized) + + # 3. Split into signal (2*d) and inner gate logits (2*d); apply inner gate + latent = self._latent_channels + signal, gate_logits = bundled.split(2 * latent, dim=-1) # DTensor ops + x = signal * gate_logits.sigmoid() + + # 4. Apply visibility mask + if mask is not None: + x = x * mask.unsqueeze(-1) + + # 5. Distributed triangle multiplication + contracted = _TriangleMultiplicativeBlockImpl.apply( + x, self.ring_comm, self._direction + ) + + # 6. Norm + output projection + out = self.proj_emit(self.norm_mix(contracted)) + + # 7. Output gate (applied to pre-norm input) + output_gate = self.proj_gate(normalized).sigmoid() + return out * output_gate diff --git a/src/transformers/models/esmfold2/distributed/utils.py b/src/transformers/models/esmfold2/distributed/utils.py new file mode 100644 index 000000000000..a4ba6e7e6089 --- /dev/null +++ b/src/transformers/models/esmfold2/distributed/utils.py @@ -0,0 +1,483 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Layout / sharding helpers and the user-facing model-wrap entry point for +2D context parallelism.""" + +from math import isqrt, lcm +from typing import Sequence + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.distributed.tensor import Shard, distribute_tensor + + +class LayoutMap: + """Bijective mapping between multi-dimensional indices and flat indices. + + Analogous to C++ std::layout_stride::mapping. + + Parameters + ---------- + strides: + Per-dimension strides (must be positive integers). + shape: + Per-dimension sizes (must be positive integers). + offset: + Offset added to every flat index (default 0). + """ + + def __init__( + self, strides: tuple[int, ...], shape: tuple[int, ...], offset: int = 0 + ): + if not all(isinstance(s, (int, np.int64)) and s > 0 for s in strides): # type: ignore[arg-type] + raise ValueError(f"Strides must be positive integers: {strides}") + if any(s < 0 for s in shape): + raise ValueError(f"Shape must be non-negative: {shape}") + if any(s == 0 for s in shape): + raise ValueError(f"Shape must not contain zeros: {shape}") + + self._strides = strides + self._n_axes = len(strides) + if len(shape) != self._n_axes: + raise ValueError( + f"Shape {shape} and strides {strides} must have the same length" + ) + + self._shape = shape + self._numel = int(np.prod(self._shape)) + self._offset = offset + + shape_and_strides = np.array( + list(zip(self._shape, self._strides)), + dtype=np.dtype([("shape", int), ("strides", int)]), + ) + argsort_ascend = np.argsort(shape_and_strides, order=["strides", "shape"]) + + self.is_unique = self._is_unique(argsort_ascend) + self.is_exhaustive = self._is_exhaustive(argsort_ascend) + + if not self.is_unique: + raise ValueError( + f"Strides {strides} and shape {shape} do not give a unique layout." + ) + + self._required_span_size = self._compute_required_span_size() + self._argsort_descend_strides = argsort_ascend[::-1] + self._argsort_ascend_strides = argsort_ascend + + def _compute_required_span_size(self) -> int: + if self._n_axes == 0: + return 1 + return 1 + sum( + (self._shape[i] - 1) * self._strides[i] for i in range(self._n_axes) + ) + + def _strides_exhaustive(self, permutation: np.ndarray): + strides = np.array(self._strides) + shape = np.array(self._shape) + shape_permuted = shape[permutation] + strides_permuted = strides[permutation] + shape_shifted = np.concatenate([[1], shape_permuted[:-1]]) + strides_shifted = np.concatenate([[1], strides_permuted[:-1]]) + return strides_permuted, strides_shifted * shape_shifted + + def _is_unique(self, permutation: np.ndarray) -> bool: + if self._n_axes == 0: + return True + strides, strides_exhaustive = self._strides_exhaustive(permutation) + return bool(np.all(strides >= strides_exhaustive)) + + def _is_exhaustive(self, permutation: np.ndarray) -> bool: + if self._n_axes == 0: + return True + strides, strides_exhaustive = self._strides_exhaustive(permutation) + return bool(np.all(strides == strides_exhaustive)) + + @property + def offset(self) -> int: + return self._offset + + @property + def required_span_size(self) -> int: + return self._required_span_size + + @property + def numel(self) -> int: + return self._numel + + @property + def shape(self) -> tuple[int, ...]: + return self._shape + + @property + def strides(self) -> tuple[int, ...]: + return self._strides + + def __call__(self, ids: tuple[int, ...]) -> int: + if len(ids) != self._n_axes: + raise ValueError( + f"Expected {self._n_axes} elements in ids but got {len(ids)}" + ) + if len(ids) == 0: + return self._offset + if self._shape is not None: + for axis, idx in enumerate(ids): + if idx < 0 or idx >= self._shape[axis]: + raise ValueError( + f"ids[{axis}] == {idx} out of range [0, {self._shape[axis] - 1}]" + ) + return int(np.dot(ids, self._strides)) + self._offset + + def unravel(self, flat_index: int) -> tuple[int, ...]: + if not self.is_unique: + raise ValueError(f"Layout is not unique, cannot unravel {flat_index}") + if not isinstance(flat_index, (int, np.integer)): + raise TypeError(f"Expected int, got {type(flat_index)}") + remaining = flat_index - self._offset + if remaining < 0 or remaining >= self._required_span_size: + raise ValueError( + f"flat_index {flat_index} out of range [{self._offset}, " + f"{self._offset + self._required_span_size - 1}]" + ) + indices = [0] * self._n_axes + for i_dim in self._argsort_descend_strides: + stride = self._strides[i_dim] + size = self._shape[i_dim] + indices[i_dim] = (remaining // stride) % size + remaining -= indices[i_dim] * stride + if remaining != 0: + raise ValueError(f"flat_index {flat_index} is out of the valid span range.") + return tuple(indices) + + def __getitem__(self, slices) -> "LayoutMap": + if not isinstance(slices, tuple) and isinstance(slices, (slice, int)): + slices = (slices,) + if len(slices) < self._n_axes: + slices = slices + (slice(None),) * (self._n_axes - len(slices)) + + new_shape = [] + new_strides = [] + new_offset = self.offset + + for axis, s in enumerate(slices): + if isinstance(s, (int, np.int64)): # type: ignore[arg-type] + new_offset += s * self.strides[axis] # type: ignore[operator] + elif isinstance(s, slice): + start, stop, step = s.indices(self.shape[axis]) + if step <= 0: + raise ValueError("Unsupported slicing: negative or zero steps") + if start >= stop: + raise ValueError("Unsupported slicing: start not smaller than stop") + dim_len = max(0, (stop - start + step - 1) // step) + new_shape.append(dim_len) + new_strides.append(self.strides[axis] * step) + new_offset += start * self.strides[axis] + else: + raise TypeError(f"Unsupported slice type: {type(s)}") + + return LayoutMap(tuple(new_strides), tuple(new_shape), new_offset) + + +class LayoutRightMap(LayoutMap): + """Row-major (C-contiguous) layout.""" + + def __init__(self, shape: tuple[int, ...]): + strides = np.ones_like(shape) + strides[1:] = shape[:0:-1] + strides = np.cumprod(strides)[::-1] + super().__init__(tuple(strides), shape=shape) + + +class LayoutLeftMap(LayoutMap): + """Column-major (Fortran-contiguous) layout.""" + + def __init__(self, shape: tuple[int, ...]): + strides = np.ones_like(shape) + strides[1:] = shape[:-1] + strides = np.cumprod(strides) + super().__init__(tuple(strides), shape=shape) + + +def get_group_rank_from_axial_shift( + coord: tuple[int, ...], axis: int, delta: int, layout_group: LayoutMap +) -> int: + """Return the rank obtained by shifting coord along axis by delta (wrapping).""" + if len(coord) != len(layout_group.shape): + raise ValueError( + f"Incompatible coord {coord} and layout_group shape {layout_group.shape}" + ) + if axis >= len(coord): + raise ValueError(f"Axis {axis} out of range for coord {coord}") + coord_shifted = list(coord) + coord_shifted[axis] = (coord_shifted[axis] + delta) % layout_group.shape[axis] + return layout_group(coord_shifted) # type: ignore[arg-type] + + +def update_exhaustive_strides( + shape_original: Sequence[int], + strides_original: Sequence[int], + shape_new: Sequence[int], +) -> tuple[int, ...]: + """Compute strides for shape_new that preserve the same axis-ordering as + the exhaustive layout (shape_original, strides_original).""" + layout_original = LayoutMap(tuple(strides_original), tuple(shape_original)) + if not layout_original.is_exhaustive: + raise ValueError( + f"Layout (shape={shape_original}, strides={strides_original}) is not exhaustive" + ) + shape_new_ascending = np.array(shape_new)[layout_original._argsort_ascend_strides] + argsort_output = np.argsort(layout_original._argsort_ascend_strides) + strides_new_ascending = np.concatenate(([1], shape_new_ascending[:-1])).cumprod() + strides_new = strides_new_ascending[argsort_output] + return tuple(strides_new.tolist()) + + +def slice_repr_mask( + s: torch.Tensor, + z: torch.Tensor, + mask: torch.Tensor, + pair_mask: torch.Tensor, + n_ranks: int, + layout_group: LayoutMap, +) -> tuple[ + list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], list[torch.Tensor] +]: + """Slice s, z, mask, pair_mask into n_ranks shards for 2D CP distribution.""" + if z.shape[-2] != z.shape[-3]: + raise ValueError(f"z is not square in the middle two axes: {z.shape}") + if s.shape[-2] != z.shape[-3]: + raise ValueError(f"Incompatible s {s.shape} and z {z.shape}") + if mask.shape != s.shape[:-1]: + raise ValueError(f"Incompatible s {s.shape} and mask {mask.shape}") + if pair_mask.shape != z.shape[:-1]: + raise ValueError(f"Incompatible z {z.shape} and pair_mask {pair_mask.shape}") + + n_tokens = s.shape[-2] + coords = [layout_group.unravel(rank) for rank in range(n_ranks)] + n_ranks_axis = isqrt(n_ranks) + if n_ranks_axis * n_ranks_axis != n_ranks: + raise ValueError(f"n_ranks is not a perfect square: {n_ranks}") + if n_tokens % n_ranks_axis: + raise ValueError( + f"Token dim {n_tokens} not divisible by sqrt(n_ranks) = {n_ranks_axis}" + ) + stride = n_tokens // n_ranks_axis + s_slices, z_slices, mask_slices, pair_mask_slices = [], [], [], [] + for i_row, j_col in coords: + i0, i1 = i_row * stride, (i_row + 1) * stride + j0, j1 = j_col * stride, (j_col + 1) * stride + s_slices.append(s[..., i0:i1, :].contiguous()) + mask_slices.append(mask[..., i0:i1].contiguous()) + z_slices.append(z[..., i0:i1, j0:j1, :].contiguous()) + pair_mask_slices.append(pair_mask[..., i0:i1, j0:j1].contiguous()) + return s_slices, z_slices, mask_slices, pair_mask_slices + + +def tiled_softmax_attention_update( + o_chunk: torch.Tensor, + lse_m_chunk: torch.Tensor, + amax_chunk: torch.Tensor | None, + o: torch.Tensor | None = None, + lse_m: torch.Tensor | None = None, + amax: torch.Tensor | None = None, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]: + """Numerically-stable online softmax accumulation for ring attention. + + Updates running (o, lse_m, amax) with a new (o_chunk, lse_m_chunk, amax_chunk). + When amax_chunk is None the function operates without amax tracking. + """ + if not ((o is None) == (lse_m is None)): + raise ValueError("o and lse_m must both be None or both provided") + + has_amax = amax_chunk is not None + is_initial_chunk = o is None + + if lse_m_chunk.shape[-1] != 1: + raise ValueError("lse_m_chunk must have shape (..., 1)") + if o_chunk.ndim != lse_m_chunk.ndim: + raise ValueError("o_chunk and lse_m_chunk must have the same ndim") + if lse_m_chunk.shape[:-1] != o_chunk.shape[:-1]: + raise ValueError("o_chunk and lse_m_chunk must match except in the last dim") + + if is_initial_chunk: + return o_chunk, lse_m_chunk, amax_chunk + + if has_amax: + d_lse_m = lse_m - lse_m_chunk # type: ignore[operator] + amax_next = torch.maximum(amax_chunk, amax) # type: ignore[arg-type] + delta_lse = amax_chunk - amax - d_lse_m # type: ignore[operator] + o_new = o - torch.sigmoid(delta_lse) * (o - o_chunk) + lse_m_new = lse_m_chunk + torch.logsumexp( + torch.cat([(amax - amax_next) + d_lse_m, amax_chunk - amax_next], dim=-1), # type: ignore[operator] + dim=-1, + keepdim=True, + ).to(dtype=lse_m_chunk.dtype) + return o_new, lse_m_new, amax_next + else: + d_lse_m = lse_m - lse_m_chunk # type: ignore[operator] + o_new = o - torch.sigmoid(-d_lse_m) * (o - o_chunk) + lse_m_new = lse_m_chunk + torch.log1p(torch.exp(d_lse_m)).to( + dtype=lse_m_chunk.dtype + ) + return o_new, lse_m_new, None + + +# --------------------------------------------------------------------------- +# End-to-end CP runtime: drop-in replacement for the serial ``FoldingTrunk``. +# --------------------------------------------------------------------------- + + +class TrunkCPWrapper(nn.Module): + """Drop-in replacement for ``FoldingTrunk`` that runs distributed. + + Accepts and returns plain tensors so the rest of an ESMFold2 model + (LM, MSA encoder, diffusion sampler) is untouched. Pair tensors whose + ``N`` is not a multiple of the CP axes are zero-padded; the gathered + output is sliced back to the original length, and the mask is padded + the same way so padded rows/cols contribute nothing. + + Typical use on an ``N×N`` CP grid (e.g. 4 ranks via + ``torch.multiprocessing.spawn``):: + + from collections import OrderedDict + + from transformers.models.esmc import ESMFold2Model + from transformers.models.esmc.distributed import ( + DistributedManager, + wrap_model_with_cp_trunks, + ) + + DistributedManager.initialize(OrderedDict([("dp", 1), ("cp", (2, 2))])) + dm = DistributedManager() + + model = ESMFold2Model.from_pretrained(...).cuda().eval() + wrap_model_with_cp_trunks(model, dm) + # ``model.forward`` (and ``processor.fold``) now run the Pairformer + # across the CP grid; everything else stays serial per rank. + """ + + def __init__(self, serial_trunk: nn.Module, dist_manager) -> None: + super().__init__() + # Lazy imports: this module is imported by manager.py and the + # distributed layers, so importing pairformer / model_common at + # module level would create a cycle. + from projects.huggingface.transformers.models.esmfold2.distributed.manager import ( + DistributedManager, + ) + from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.pairformer import ( + FoldingTrunkDistributed, + ) + from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( + FoldingTrunk as SerialFoldingTrunk, + ) + + if not isinstance(serial_trunk, SerialFoldingTrunk): + raise TypeError(f"expected FoldingTrunk, got {type(serial_trunk).__name__}") + if not isinstance(dist_manager, DistributedManager): + raise TypeError( + f"expected DistributedManager, got {type(dist_manager).__name__}" + ) + + # ``FoldingTrunkDistributed`` requires the serial trunk's tri-mul + # kernels off and chunking disabled (it composes its own ring loop). + # ``serial_trunk`` is typed ``nn.Module`` (the SerialFoldingTrunk + # symbol is imported lazily inside the function to avoid a circular + # import with pairformer.py); pyright can't narrow through the + # lazy-import isinstance check. + serial_trunk.set_kernel_backend(None) # type: ignore[operator] + serial_trunk.set_chunk_size(None) # type: ignore[operator] + + self.dist_trunk = FoldingTrunkDistributed(serial_trunk, dist_manager) + self.dist_manager = dist_manager + self.device_mesh = dist_manager.device_mesh_subgroups + # device_mesh is (dp, cp_axis_0, cp_axis_1) + self.cp_axis_0 = self.device_mesh.size(1) + self.cp_axis_1 = self.device_mesh.size(2) + self.shard_factor = lcm(self.cp_axis_0, self.cp_axis_1) + + # The serial trunk exposes these knobs to the parent model. The + # distributed path doesn't support kernels/chunking, but the parent + # ``set_kernel_backend`` / ``set_chunk_size`` calls still need a no-op + # hook so they don't blow up. + def set_kernel_backend(self, _backend: str | None) -> None: + return + + def set_chunk_size(self, _chunk_size: int | None) -> None: + return + + def forward( + self, pair: torch.Tensor, pair_attention_mask: torch.Tensor | None = None + ) -> torch.Tensor: + N = pair.shape[1] + pad = (self.shard_factor - N % self.shard_factor) % self.shard_factor + if pad: + # F.pad pads from the last dim backward; pair is (B, N, N, d_pair). + pair = F.pad(pair, (0, 0, 0, pad, 0, pad)) + if pair_attention_mask is not None: + pair_attention_mask = F.pad(pair_attention_mask, (0, pad, 0, pad)) + + pair_dt = distribute_tensor( + pair.contiguous(), self.device_mesh, [Shard(0), Shard(1), Shard(2)] + ) + mask_dt = None + if pair_attention_mask is not None: + mask_dt = distribute_tensor( + pair_attention_mask.contiguous(), + self.device_mesh, + [Shard(0), Shard(1), Shard(2)], + ) + + out_dt = self.dist_trunk(pair_dt, pair_attention_mask=mask_dt) + out = out_dt.full_tensor() + if pad: + out = out[:, :N, :N, :] + return out + + +def wrap_model_with_cp_trunks(model: nn.Module, dist_manager) -> list[str]: + """Replace every ``FoldingTrunk`` submodule with ``TrunkCPWrapper``. + + Walks ``model.named_modules()`` and rebinds each attribute that points + at a serial ``FoldingTrunk``. Returns the list of replaced submodule + paths so callers (e.g. spawned workers) can log what got wrapped. + """ + from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( + FoldingTrunk as SerialFoldingTrunk, + ) + + targets: list[tuple[str, nn.Module, str, nn.Module]] = [] + for parent_name, parent in model.named_modules(): + for child_name, child in parent.named_children(): + if isinstance(child, SerialFoldingTrunk): + full = f"{parent_name}.{child_name}" if parent_name else child_name + targets.append((full, parent, child_name, child)) + + replaced: list[str] = [] + for full, parent, child_name, child in targets: + wrapped = TrunkCPWrapper(child, dist_manager).to( + device=dist_manager.device, dtype=next(child.parameters()).dtype + ) + setattr(parent, child_name, wrapped) + replaced.append(full) + return replaced diff --git a/src/transformers/models/esmfold2/kernels/__init__.py b/src/transformers/models/esmfold2/kernels/__init__.py new file mode 100644 index 000000000000..6705f4936c9b --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/__init__.py @@ -0,0 +1,21 @@ +# coding=utf-8 +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +"""Triton inference kernels for ESMFold2.""" + +from .fused_attention_pair_bias import fused_pair_bias +from .fused_dropout_residual import FusedDropoutResidual +from .fused_lnlin_swiglu import FusedLNLinearSwiGLU +from .trimul_with_residual import triangle_multiplicative_update_with_residual + +__all__ = [ + "fused_pair_bias", + "FusedDropoutResidual", + "FusedLNLinearSwiGLU", + "triangle_multiplicative_update_with_residual", +] diff --git a/src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py b/src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py new file mode 100644 index 000000000000..98b722397bb7 --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py @@ -0,0 +1,697 @@ +"""Fused Triton kernel for AttentionPairBias (forward + backward). + +Inspired by cuequivariance's ``attention_pair_bias`` +(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently +re-implemented in Triton with a backward pass and no sequence-length gate. + +Fuses ``LayerNorm(z) -> z @ w_proj_z.T -> (1-mask)*(-INF)`` into a single +kernel that emits a ``bias[B, H, Q, K]`` tensor, then dispatches the +attention itself to ``torch.nn.functional.scaled_dot_product_attention``. +""" + +# ruff: noqa: E402 + +import os +import warnings + +warnings.filterwarnings("ignore", category=FutureWarning) +warnings.filterwarnings("ignore", category=UserWarning) +warnings.filterwarnings("ignore", category=DeprecationWarning) +os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1") +os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1") + +import torch +import triton +import triton.language as tl + +# Static config — runtime autotune cold-start is unshippable for inference. +_BIAS_AUTOTUNE_CONFIGS = [ + triton.Config({"TILE_K": 64, "TILE_C": 64}, num_stages=3, num_warps=4) +] + + +@triton.autotune( + configs=_BIAS_AUTOTUNE_CONFIGS, + key=["Q", "K", "DIM_Z", "NUM_HEADS", "HEADS_PER_BLK", "HAS_MASK", "AFFINE"], +) +@triton.jit +def _pair_bias_kernel( + z_ptr, # [B, Q, K, DIM_Z] pair tensor, bf16/fp16/fp32 + mask_ptr, # [B, K] key padding mask (bool/uint8) + w_proj_z_ptr, # [NUM_HEADS, DIM_Z] bias-projection weight + w_ln_ptr, # [DIM_Z] LN gamma (or unused) + b_ln_ptr, # [DIM_Z] LN beta (or unused) + out_ptr, # [B, NUM_HEADS, Q, K] fused output bias, same dtype as z + mean_ptr, # [B, Q, K] saved LN mean (fp32) or dummy + rstd_ptr, # [B, Q, K] saved LN rstd (fp32) or dummy + B, + Q, + K, + NEG_INF: tl.constexpr, + EPS: tl.constexpr, + DIM_Z: tl.constexpr, + DIM_Z_PAD: tl.constexpr, + NUM_HEADS: tl.constexpr, + HEADS_PER_BLK: tl.constexpr, + TILE_K: tl.constexpr, + TILE_C: tl.constexpr, # tile over the DIM_Z reduction axis + HAS_MASK: tl.constexpr, + AFFINE: tl.constexpr, + SAVE_STATS: tl.constexpr, +): + """One CTA owns (one Q-row × TILE_K K-positions × HEADS_PER_BLK heads). + + Grid: (cdiv(K, TILE_K), Q, B * cdiv(NUM_HEADS, HEADS_PER_BLK)). + + Each thread block computes a tile of the output bias + ``bias[b, h_blk, q, k_tile]`` and stores it to ``out_ptr``. Layout of + ``out_ptr`` is ``[B, NUM_HEADS, Q, K]`` so the downstream SDPA call can + pass it directly as ``attn_mask`` (broadcast-compatible with the standard + BHQK attention layout). + + When ``SAVE_STATS`` is set (training path), the per-row ``mean`` and + ``rstd`` are stored into ``mean_ptr``/``rstd_ptr`` of shape ``[B, Q, K]`` + so the backward kernel can avoid recomputing them. Only the first head-block + in each (B, Q, K) tile writes the stats to avoid races. + """ + pid_k = tl.program_id(0) + pid_q = tl.program_id(1) + pid_bh = tl.program_id(2) + NUM_HEAD_BLKS: tl.constexpr = (NUM_HEADS + HEADS_PER_BLK - 1) // HEADS_PER_BLK + pid_b = pid_bh // NUM_HEAD_BLKS + pid_hblk = pid_bh % NUM_HEAD_BLKS + + offs_k = pid_k * TILE_K + tl.arange(0, TILE_K) + offs_z_full = tl.arange(0, DIM_Z_PAD) + offs_h = pid_hblk * HEADS_PER_BLK + tl.arange(0, HEADS_PER_BLK) + mask_k = offs_k < K + mask_h = offs_h < NUM_HEADS + + z_full_ptrs = ( + z_ptr + + pid_b * Q * K * DIM_Z + + pid_q * K * DIM_Z + + offs_k[:, None] * DIM_Z + + offs_z_full[None, :] + ) + mask_z_full = offs_z_full < DIM_Z + z_full = tl.load( + z_full_ptrs, mask=mask_k[:, None] & mask_z_full[None, :], other=0.0 + ).to(tl.float32) + + mean = tl.sum(z_full, axis=1) / DIM_Z + z_centered = z_full - mean[:, None] + z_centered = tl.where(mask_z_full[None, :], z_centered, 0.0) + var = tl.sum(z_centered * z_centered, axis=1) / DIM_Z + rstd = 1.0 / tl.sqrt(var + EPS) + + # Save mean/rstd for backward — only the first head-block writes, all + # head-blocks have the same value so this avoids racy duplicate writes. + if SAVE_STATS: + if pid_hblk == 0: + stats_ptrs = mean_ptr + pid_b * Q * K + pid_q * K + offs_k + tl.store(stats_ptrs, mean, mask=mask_k) + stats_ptrs2 = rstd_ptr + pid_b * Q * K + pid_q * K + offs_k + tl.store(stats_ptrs2, rstd, mask=mask_k) + + acc = tl.zeros([TILE_K, HEADS_PER_BLK], dtype=tl.float32) + num_tiles_c = tl.cdiv(DIM_Z, TILE_C) + for tc in range(0, num_tiles_c): + offs_c = tc * TILE_C + tl.arange(0, TILE_C) + mask_c = offs_c < DIM_Z + + z_slice_ptrs = ( + z_ptr + + pid_b * Q * K * DIM_Z + + pid_q * K * DIM_Z + + offs_k[:, None] * DIM_Z + + offs_c[None, :] + ) + z_slice = tl.load( + z_slice_ptrs, mask=mask_k[:, None] & mask_c[None, :], other=0.0 + ).to(tl.float32) + + z_norm = (z_slice - mean[:, None]) * rstd[:, None] + if AFFINE: + gamma = tl.load(w_ln_ptr + offs_c, mask=mask_c, other=1.0).to(tl.float32) + beta = tl.load(b_ln_ptr + offs_c, mask=mask_c, other=0.0).to(tl.float32) + z_norm = z_norm * gamma[None, :] + beta[None, :] + z_norm = tl.where(mask_c[None, :], z_norm, 0.0) + + w_ptrs = w_proj_z_ptr + (offs_h[None, :] * DIM_Z + offs_c[:, None]) + w_tile = tl.load(w_ptrs, mask=mask_h[None, :] & mask_c[:, None], other=0.0).to( + tl.float32 + ) + + acc = tl.dot(z_norm.to(tl.float32), w_tile, acc, input_precision="tf32x3") + + if HAS_MASK: + m_tile = tl.load(mask_ptr + pid_b * K + offs_k, mask=mask_k, other=0).to( + tl.int32 + ) + acc = acc + tl.where(m_tile == 0, NEG_INF, 0.0)[:, None] + # Mask out-of-bounds K positions (we pad to TILE_K). + acc = tl.where(mask_k[:, None], acc, NEG_INF) + + out_ptrs = ( + out_ptr + + pid_b * NUM_HEADS * Q * K + + offs_h[None, :] * Q * K + + pid_q * K + + offs_k[:, None] + ) + tl.store( + out_ptrs, + acc.to(out_ptr.type.element_ty), + mask=mask_k[:, None] & mask_h[None, :], + ) + + +# Backward is reduction-heavy (atomics into d_w_proj_z / d_pair_norm_*); modest +# TILE_K=32 balances ILP and register pressure on Hopper. +_BIAS_BWD_CONFIGS = [triton.Config({"TILE_K": 32}, num_stages=3, num_warps=4)] + + +@triton.autotune( + configs=_BIAS_BWD_CONFIGS, key=["Q", "K", "DIM_Z", "NUM_HEADS", "AFFINE"] +) +@triton.jit +def _pair_bias_backward_kernel( + z_ptr, # [B, Q, K, DIM_Z] input pair tensor (bf16) + w_proj_z_ptr, # [NUM_HEADS, DIM_Z] bias-projection weight (bf16) + w_ln_ptr, # [DIM_Z] LN gamma (bf16, or dummy) + b_ln_ptr, # [DIM_Z] LN beta (bf16, or dummy) + mean_ptr, # [B, Q, K] saved LN mean (fp32) + rstd_ptr, # [B, Q, K] saved LN rstd (fp32) + d_bias_ptr, # [B, NUM_HEADS, Q, K] upstream gradient (bf16) + d_z_ptr, # [B, Q, K, DIM_Z] output d_z (bf16) + d_w_proj_z_ptr, # [NUM_HEADS, DIM_Z] output d_w_proj_z (fp32 accum) + d_ln_w_ptr, # [DIM_Z] output d_pair_norm_w (fp32 accum) + d_ln_b_ptr, # [DIM_Z] output d_pair_norm_b (fp32 accum) + Q, + K, + EPS: tl.constexpr, + DIM_Z: tl.constexpr, + DIM_Z_PAD: tl.constexpr, + NUM_HEADS: tl.constexpr, + NUM_HEADS_PAD: tl.constexpr, + TILE_K: tl.constexpr, + AFFINE: tl.constexpr, +): + """Backward for ``fused_pair_bias``. + + Grid: (cdiv(K, TILE_K), Q, B). + + Each CTA processes a (b, q, k_tile) slab and: + + 1. Loads ``z[b,q,k,:]``, ``mean``, ``rstd``, ``d_bias[b,:,q,k]``, + ``w_proj_z[:,:]``, ``gamma``, ``beta``. + 2. Computes ``z_hat = (z - mean) * rstd``, ``z_norm = z_hat * gamma + beta``. + 3. ``d_z_norm[k,c] = sum_h d_bias[h,k] * w_proj_z[h,c]`` (matmul, k×c). + 4. Atomic-add ``d_w_proj_z[h,c] += d_bias[k,h]^T @ z_norm[k,c]``. + 5. Atomic-add ``d_pair_norm_b[c] += sum_k d_z_norm[k,c]``. + 6. Atomic-add ``d_pair_norm_w[c] += sum_k d_z_norm[k,c] * z_hat[k,c]``. + 7. ``d_z_hat = d_z_norm * gamma``. + 8. LN bwd: ``d_z = (d_z_hat - mean_c(d_z_hat) - z_hat * mean_c(d_z_hat * z_hat)) * rstd``. + 9. Store ``d_z``. + + All math is fp32 internally; only loads/stores are bf16. Atomic adds + target fp32 buffers (bf16 atomics are not supported on Hopper). + """ + pid_k = tl.program_id(0) + pid_q = tl.program_id(1) + pid_b = tl.program_id(2) + + offs_k = pid_k * TILE_K + tl.arange(0, TILE_K) + offs_z = tl.arange(0, DIM_Z_PAD) + offs_h = tl.arange(0, NUM_HEADS_PAD) + mask_k = offs_k < K + mask_z = offs_z < DIM_Z + mask_h = offs_h < NUM_HEADS + + z_ptrs = ( + z_ptr + + pid_b * Q * K * DIM_Z + + pid_q * K * DIM_Z + + offs_k[:, None] * DIM_Z + + offs_z[None, :] + ) + z = tl.load(z_ptrs, mask=mask_k[:, None] & mask_z[None, :], other=0.0).to( + tl.float32 + ) + mean_ptrs = mean_ptr + pid_b * Q * K + pid_q * K + offs_k + rstd_ptrs = rstd_ptr + pid_b * Q * K + pid_q * K + offs_k + mean = tl.load(mean_ptrs, mask=mask_k, other=0.0) + rstd = tl.load(rstd_ptrs, mask=mask_k, other=0.0) + + z_hat = (z - mean[:, None]) * rstd[:, None] + z_hat = tl.where(mask_k[:, None] & mask_z[None, :], z_hat, 0.0) + + if AFFINE: + gamma = tl.load(w_ln_ptr + offs_z, mask=mask_z, other=1.0).to(tl.float32) + beta = tl.load(b_ln_ptr + offs_z, mask=mask_z, other=0.0).to(tl.float32) + else: + gamma = tl.full([DIM_Z_PAD], 1.0, dtype=tl.float32) + beta = tl.full([DIM_Z_PAD], 0.0, dtype=tl.float32) + # Recompute normalized output (needed for d_w_proj_z). + z_norm = z_hat * gamma[None, :] + beta[None, :] + z_norm = tl.where(mask_k[:, None] & mask_z[None, :], z_norm, 0.0) + + d_bias_ptrs = ( + d_bias_ptr + + pid_b * NUM_HEADS * Q * K + + offs_h[None, :] * Q * K + + pid_q * K + + offs_k[:, None] + ) + d_bias = tl.load(d_bias_ptrs, mask=mask_k[:, None] & mask_h[None, :], other=0.0).to( + tl.float32 + ) + + w_proj_ptrs = w_proj_z_ptr + offs_h[:, None] * DIM_Z + offs_z[None, :] + w_proj = tl.load(w_proj_ptrs, mask=mask_h[:, None] & mask_z[None, :], other=0.0).to( + tl.float32 + ) + + d_z_norm = tl.dot(d_bias, w_proj, input_precision="tf32x3") + d_z_norm = tl.where(mask_k[:, None] & mask_z[None, :], d_z_norm, 0.0) + + d_w = tl.dot(tl.trans(d_bias), z_norm, input_precision="tf32x3") + d_w_ptrs = d_w_proj_z_ptr + offs_h[:, None] * DIM_Z + offs_z[None, :] + tl.atomic_add(d_w_ptrs, d_w, mask=mask_h[:, None] & mask_z[None, :], sem="relaxed") + + if AFFINE: + d_b_tile = tl.sum(d_z_norm, axis=0) + tl.atomic_add(d_ln_b_ptr + offs_z, d_b_tile, mask=mask_z, sem="relaxed") + + d_w_tile = tl.sum(d_z_norm * z_hat, axis=0) + tl.atomic_add(d_ln_w_ptr + offs_z, d_w_tile, mask=mask_z, sem="relaxed") + + d_z_hat = d_z_norm * gamma[None, :] + d_z_hat = tl.where(mask_k[:, None] & mask_z[None, :], d_z_hat, 0.0) + sum_dzh = tl.sum(d_z_hat, axis=1) + sum_dzh_zhat = tl.sum(d_z_hat * z_hat, axis=1) + mean_dzh = sum_dzh / DIM_Z + mean_dzh_zhat = sum_dzh_zhat / DIM_Z + d_z = (d_z_hat - mean_dzh[:, None] - z_hat * mean_dzh_zhat[:, None]) * rstd[:, None] + d_z = tl.where(mask_k[:, None] & mask_z[None, :], d_z, 0.0) + + d_z_ptrs = ( + d_z_ptr + + pid_b * Q * K * DIM_Z + + pid_q * K * DIM_Z + + offs_k[:, None] * DIM_Z + + offs_z[None, :] + ) + tl.store( + d_z_ptrs, + d_z.to(d_z_ptr.type.element_ty), + mask=mask_k[:, None] & mask_z[None, :], + ) + + +def _next_pow2(x: int) -> int: + p = 1 + while p < x: + p *= 2 + return max(p, 16) + + +def _round_up_heads_per_blk(num_heads: int) -> int: + """Always return 16 — ``tl.dot`` requires M, N, K >= 16 on Hopper. + + For the AF3-style transformer ``num_heads=16``; ``HEADS_PER_BLK=16`` gives + a single CTA per (b, q, k_tile) and is the cheapest schedule. Head-counts + below 16 are zero-padded inside the kernel. + """ + del num_heads # head-tile is fixed at 16 by the tl.dot lower bound + return 16 + + +def _launch_forward( + z: torch.Tensor, + mask: torch.Tensor | None, + w_proj_z: torch.Tensor, + pair_norm_w: torch.Tensor | None, + pair_norm_b: torch.Tensor | None, + num_heads: int, + eps: float, + inf: float, + save_stats: bool, +) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]: + """Forward kernel launch helper. Returns ``(bias, mean, rstd)``. + + ``mean``/``rstd`` are ``None`` unless ``save_stats=True``. + """ + assert z.dim() == 4, f"z must be (B,Q,K,DIM_Z); got {z.shape}" + B, Q, K, DIM_Z = z.shape + assert w_proj_z.shape == ( + num_heads, + DIM_Z, + ), f"w_proj_z {w_proj_z.shape} ≠ ({num_heads}, {DIM_Z})" + + z = z.contiguous() + w_proj_z = w_proj_z.contiguous() + affine = pair_norm_w is not None or pair_norm_b is not None + if affine: + if pair_norm_w is None: + pair_norm_w = torch.ones(DIM_Z, device=z.device, dtype=z.dtype) + if pair_norm_b is None: + pair_norm_b = torch.zeros(DIM_Z, device=z.device, dtype=z.dtype) + pair_norm_w = pair_norm_w.contiguous() + pair_norm_b = pair_norm_b.contiguous() + if mask is not None: + mask = mask.contiguous() + assert mask.shape == (B, K), f"mask {mask.shape} ≠ ({B}, {K})" + + out = torch.empty((B, num_heads, Q, K), device=z.device, dtype=z.dtype) + if save_stats: + mean = torch.empty((B, Q, K), device=z.device, dtype=torch.float32) + rstd = torch.empty((B, Q, K), device=z.device, dtype=torch.float32) + else: + mean = None + rstd = None + heads_per_blk = _round_up_heads_per_blk(num_heads) + DIM_Z_PAD = _next_pow2(DIM_Z) + _dummy = torch.empty(1, device=z.device, dtype=z.dtype) + _dummy_f32 = torch.empty(1, device=z.device, dtype=torch.float32) + num_head_blks = (num_heads + heads_per_blk - 1) // heads_per_blk + grid = lambda meta: (triton.cdiv(K, meta["TILE_K"]), Q, B * num_head_blks) + _pair_bias_kernel[grid]( + z, + mask if mask is not None else _dummy, + w_proj_z, + pair_norm_w if affine else _dummy, + pair_norm_b if affine else _dummy, + out, + mean if save_stats else _dummy_f32, + rstd if save_stats else _dummy_f32, + B, + Q, + K, + NEG_INF=-float(inf), + EPS=eps, + DIM_Z=DIM_Z, + DIM_Z_PAD=DIM_Z_PAD, + NUM_HEADS=num_heads, + HEADS_PER_BLK=heads_per_blk, + HAS_MASK=mask is not None, + AFFINE=affine, + SAVE_STATS=save_stats, + ) + return out, mean, rstd + + +def _launch_backward( + z: torch.Tensor, + w_proj_z: torch.Tensor, + pair_norm_w: torch.Tensor | None, + pair_norm_b: torch.Tensor | None, + mean: torch.Tensor, + rstd: torch.Tensor, + d_bias: torch.Tensor, + eps: float, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None]: + """Backward kernel launch helper. Returns ``(d_z, d_w_proj_z, d_pair_norm_w, d_pair_norm_b)``. + + The latter two are ``None`` when affine is off. + """ + B, Q, K, DIM_Z = z.shape + NUM_HEADS = w_proj_z.shape[0] + affine = pair_norm_w is not None + if affine: + assert pair_norm_b is not None + + z = z.contiguous() + w_proj_z = w_proj_z.contiguous() + d_bias = d_bias.contiguous() + if affine: + assert pair_norm_w is not None and pair_norm_b is not None + pair_norm_w = pair_norm_w.contiguous() + pair_norm_b = pair_norm_b.contiguous() + + d_z = torch.empty_like(z) + # fp32 accumulators for atomic adds — bf16 atomic_add is not supported on Hopper + d_w_proj_z_f32 = torch.zeros( + (NUM_HEADS, DIM_Z), device=z.device, dtype=torch.float32 + ) + if affine: + d_pair_norm_w_f32 = torch.zeros(DIM_Z, device=z.device, dtype=torch.float32) + d_pair_norm_b_f32 = torch.zeros(DIM_Z, device=z.device, dtype=torch.float32) + else: + d_pair_norm_w_f32 = torch.zeros(1, device=z.device, dtype=torch.float32) + d_pair_norm_b_f32 = torch.zeros(1, device=z.device, dtype=torch.float32) + + DIM_Z_PAD = _next_pow2(DIM_Z) + # min 16 to satisfy Triton's tl.dot requirement (M, N, K >= 16 on Hopper). + NUM_HEADS_PAD = max(16, _next_pow2(NUM_HEADS)) + _dummy = torch.empty(1, device=z.device, dtype=z.dtype) + + grid = lambda meta: (triton.cdiv(K, meta["TILE_K"]), Q, B) + _pair_bias_backward_kernel[grid]( + z, + w_proj_z, + pair_norm_w if affine else _dummy, + pair_norm_b if affine else _dummy, + mean, + rstd, + d_bias, + d_z, + d_w_proj_z_f32, + d_pair_norm_w_f32, + d_pair_norm_b_f32, + Q, + K, + EPS=eps, + DIM_Z=DIM_Z, + DIM_Z_PAD=DIM_Z_PAD, + NUM_HEADS=NUM_HEADS, + NUM_HEADS_PAD=NUM_HEADS_PAD, + AFFINE=affine, + ) + + d_w_proj_z = d_w_proj_z_f32.to(w_proj_z.dtype) + d_pair_norm_w: torch.Tensor | None + d_pair_norm_b: torch.Tensor | None + if affine: + assert pair_norm_w is not None and pair_norm_b is not None + d_pair_norm_w = d_pair_norm_w_f32.to(pair_norm_w.dtype) + d_pair_norm_b = d_pair_norm_b_f32.to(pair_norm_b.dtype) + else: + d_pair_norm_w = None + d_pair_norm_b = None + + return d_z, d_w_proj_z, d_pair_norm_w, d_pair_norm_b + + +class FusedPairBias(torch.autograd.Function): + """Autograd wrapper around ``_pair_bias_kernel`` and ``_pair_bias_backward_kernel``. + + Forward saves ``(z, w_proj_z, pair_norm_w, pair_norm_b, mean, rstd)`` so that + the backward kernel can recompute ``z_hat`` without re-doing the full + LN reduction. ``mask`` is non-differentiable; we save it in ``ctx`` only as + a marker (the kernel applies it inside the forward, but mask positions get + -INF and so contribute 0 gradient through softmax → no special handling + needed for ``d_bias``). + """ + + @staticmethod + def forward(ctx, z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads, eps, inf): + out, mean, rstd = _launch_forward( + z, + mask, + w_proj_z, + pair_norm_w, + pair_norm_b, + num_heads, + eps, + inf, + save_stats=True, + ) + affine = pair_norm_w is not None or pair_norm_b is not None + ctx.save_for_backward( + z, + w_proj_z, + pair_norm_w if affine else None, + pair_norm_b if affine else None, + mean, + rstd, + ) + ctx.affine = affine + ctx.eps = eps + return out + + @staticmethod + def backward(ctx, d_bias): + z, w_proj_z, pair_norm_w, pair_norm_b, mean, rstd = ctx.saved_tensors + d_z, d_w_proj_z, d_pair_norm_w, d_pair_norm_b = _launch_backward( + z, + w_proj_z, + pair_norm_w, + pair_norm_b, + mean, + rstd, + d_bias.contiguous(), + ctx.eps, + ) + # Order must match forward args: (z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads, eps, inf) + return ( + d_z, + None, # mask + d_w_proj_z, + d_pair_norm_w, + d_pair_norm_b, + None, # num_heads + None, # eps + None, # inf + ) + + +@torch._dynamo.disable +def fused_pair_bias( + z: torch.Tensor, + mask: torch.Tensor | None, + w_proj_z: torch.Tensor, + pair_norm_w: torch.Tensor | None, + pair_norm_b: torch.Tensor | None, + *, + num_heads: int, + eps: float = 1e-5, + inf: float = 1e6, +) -> torch.Tensor: + """Compute ``bias[B, H, Q, K] = LN(z) @ w_proj_z.T + (1-mask)*-INF``. + + Dispatches to the autograd-aware ``FusedPairBias`` path when autograd is + enabled (i.e. any input requires_grad and we are not in a no_grad/inference + context). Otherwise falls back to the forward-only kernel. + + Parameters + ---------- + z : (B, Q, K, DIM_Z) + mask : (B, K) bool or None. True = keep, False = mask out (-INF added). + w_proj_z : (num_heads, DIM_Z) + pair_norm_w, pair_norm_b : (DIM_Z,) or None. Pass both or neither. + num_heads : int + eps, inf : LN epsilon and masking-infinity respectively. + + Returns + ------- + bias : (B, num_heads, Q, K) — same dtype as z. + """ + use_autograd = torch.is_grad_enabled() and ( + z.requires_grad + or w_proj_z.requires_grad + or (pair_norm_w is not None and pair_norm_w.requires_grad) + or (pair_norm_b is not None and pair_norm_b.requires_grad) + ) + if use_autograd: + out_t: torch.Tensor = FusedPairBias.apply( # type: ignore[assignment] + z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads, eps, inf + ) + return out_t + out, _, _ = _launch_forward( + z, + mask, + w_proj_z, + pair_norm_w, + pair_norm_b, + num_heads, + eps, + inf, + save_stats=False, + ) + return out + + +def fused_attention_pair_bias( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + z: torch.Tensor | None, + mask: torch.Tensor | None, + x_for_gate: torch.Tensor, + *, + w_proj_z: torch.Tensor | None, + w_proj_g: torch.Tensor, + w_proj_o: torch.Tensor, + pair_norm_w: torch.Tensor | None = None, + pair_norm_b: torch.Tensor | None = None, + eps: float = 1e-5, + inf: float = 1e6, + precomputed_bias: torch.Tensor | None = None, +) -> torch.Tensor: + """End-to-end fused AttentionPairBias forward (no-conditioning path). + + Pipeline: + bias = fused_pair_bias(z, mask, w_proj_z, ln_w, ln_b) # Triton + attn = SDPA(q, k, v, attn_mask=bias) # cuDNN / Flash + gate = sigmoid(linear(x_for_gate, w_proj_g)) # cuBLAS + out = linear(gate * attn, w_proj_o) # cuBLAS + + Parameters + ---------- + q, k, v : (B, H, Q|K, D) — already-projected query/key/value, head-split. + z : (B, Q, K, DIM_Z) — raw (unnormed) pair tensor. Ignored when + ``precomputed_bias`` is supplied. + mask : (B, K) bool or None. Ignored when ``precomputed_bias`` is supplied. + x_for_gate : (B, Q, d_model) — pre-norm input for the gate + ``sigmoid(x @ w_proj_g.T)``. In the production module this is the + adaln/pre_norm output ``x`` (same tensor that feeds ``q``). + w_proj_z : (H, DIM_Z). Ignored when ``precomputed_bias`` is supplied. + w_proj_g, w_proj_o : (d_model, d_model) + pair_norm_w, pair_norm_b : (DIM_Z,). Ignored when ``precomputed_bias`` is + supplied. + precomputed_bias : (B, H, Q, K) optional cached bias tensor. When provided + the LN+proj+mask Triton kernel is skipped entirely — the bias is reused + as the SDPA ``attn_mask`` directly. This is the 50× diffusion-step + amortization path: ``z`` is constant within a loop, so the bias can + be computed once and reused across all 50 denoise steps. Compute it + with ``fused_pair_bias`` once, then pass it back here on every step. + + Returns + ------- + out : (B, Q, d_model) + """ + B, H, Q, D = q.shape + d_model = H * D + + if precomputed_bias is not None: + assert ( + not torch.is_grad_enabled() + ), "precomputed_bias path is inference-only; autograd is not supported." + bias = precomputed_bias + else: + if z is None or w_proj_z is None: + raise ValueError( + "Either precomputed_bias OR (z, w_proj_z) must be supplied" + ) + bias = fused_pair_bias( + z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads=H, eps=eps, inf=inf + ) # (B, H, Q, K) + + # Match the dtype expected by cuDNN attention. q/k/v are already bf16 in + # inference; ``bias`` inherits z's dtype. + if not torch.compiler.is_compiling(): + with torch.nn.attention.sdpa_kernel( + backends=[ + torch.nn.attention.SDPBackend.CUDNN_ATTENTION, + torch.nn.attention.SDPBackend.FLASH_ATTENTION, + torch.nn.attention.SDPBackend.EFFICIENT_ATTENTION, + ], + set_priority=True, + ): + attn = torch.nn.functional.scaled_dot_product_attention( + q, k, v, attn_mask=bias, is_causal=False + ) + else: + attn = torch.nn.functional.scaled_dot_product_attention( + q, k, v, attn_mask=bias, is_causal=False + ) + attn = attn.transpose(1, 2).contiguous().view(B, Q, d_model) + + gate = torch.sigmoid(torch.nn.functional.linear(x_for_gate, w_proj_g)) + out = torch.nn.functional.linear(gate * attn, w_proj_o) + return out + + +__all__ = ["FusedPairBias", "fused_attention_pair_bias", "fused_pair_bias"] diff --git a/src/transformers/models/esmfold2/kernels/fused_dropout_residual.py b/src/transformers/models/esmfold2/kernels/fused_dropout_residual.py new file mode 100644 index 000000000000..2335d66fe895 --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/fused_dropout_residual.py @@ -0,0 +1,247 @@ +"""Fused row-shared-dropout + residual-add kernel for the pair stream. + +For the pairformer pattern: + + pair_new = pair + Dropout(r, batch_dim=1)(delta) + +where `delta` is the output of e.g. a triangle multiplication, the row-shared +dropout multiplies `delta` by a Bernoulli mask of shape `[B, 1, N_col, D]` +(broadcast over the row dim) scaled by `1/(1-r)`. Naively this materializes +two intermediate `[B, N, N, D]` tensors (one for `delta * mask`, one for +`pair + ...`) — three full HBM round-trips of the pair tensor. + +This kernel reads `pair`, `delta`, and the small `[N_col, D]` shared mask +(via modulo-`N_col` indexing — no broadcast materialization) and writes the +combined `pair + delta * mask` once. + +Backward is also a single Triton kernel: it mutates the saved mask buffer in +place to produce `ddelta = dout * mask`, and the residual gradient passes +through unchanged. + +Convention: the mask is expected to already be scaled (i.e. it's the output +of `nn.Dropout(r)(ones_like(...))` so that retained entries have value +`1/(1-r)`). +""" + +from __future__ import annotations + +import torch +import torch.nn as nn +import triton +import triton.language as tl + + +@triton.jit +def _fused_dropout_residual_fwd_kernel( + pair_ptr, # [M, D] M = B*N_row*N_col + delta_ptr, # [M, D] + mask_ptr, # [B*N_col, D] per-batch row-shared mask + out_ptr, # [M, D] + M, + D: tl.constexpr, + N_COL: tl.constexpr, + STRIDE_B: tl.constexpr, # = N_row * N_col, used to recover batch index + BLOCK_M: tl.constexpr, + BLOCK_D: tl.constexpr, +): + pid_m = tl.program_id(0).to(tl.int64) + pid_d = tl.program_id(1).to(tl.int64) + + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = pid_d * BLOCK_D + tl.arange(0, BLOCK_D) + mm_m = offs_m < M + mm_d = offs_d < D + + # m = b*N_row*N_col + i*N_col + j → b = m // (N_row*N_col), j = m % N_col + # mask is laid out as [B*N_col, D] so per-batch index is b*N_col + j. + b = offs_m // STRIDE_B + j = offs_m % N_COL + mask_row = b * N_COL + j + + pair_ptrs = pair_ptr + offs_m[:, None] * D + offs_d[None, :] + delta_ptrs = delta_ptr + offs_m[:, None] * D + offs_d[None, :] + out_ptrs = out_ptr + offs_m[:, None] * D + offs_d[None, :] + mask_ptrs = mask_ptr + mask_row[:, None] * D + offs_d[None, :] + + p = tl.load(pair_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) + de = tl.load(delta_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) + mk = tl.load(mask_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) + + o = p + de * mk + tl.store( + out_ptrs, o.to(out_ptr.type.element_ty), mask=mm_m[:, None] & mm_d[None, :] + ) + + +@triton.jit +def _fused_dropout_residual_bwd_kernel( + dout_ptr, # [M, D] + mask_ptr, # [B*N_col, D] + ddelta_ptr, # [M, D] + M, + D: tl.constexpr, + N_COL: tl.constexpr, + STRIDE_B: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_D: tl.constexpr, +): + pid_m = tl.program_id(0).to(tl.int64) + pid_d = tl.program_id(1).to(tl.int64) + + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_d = pid_d * BLOCK_D + tl.arange(0, BLOCK_D) + mm_m = offs_m < M + mm_d = offs_d < D + + b = offs_m // STRIDE_B + j = offs_m % N_COL + mask_row = b * N_COL + j + mask_ptrs = mask_ptr + mask_row[:, None] * D + offs_d[None, :] + + do = tl.load( + dout_ptr + offs_m[:, None] * D + offs_d[None, :], + mask=mm_m[:, None] & mm_d[None, :], + other=0.0, + ) + mk = tl.load(mask_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) + tl.store( + ddelta_ptr + offs_m[:, None] * D + offs_d[None, :], + (do * mk).to(ddelta_ptr.type.element_ty), + mask=mm_m[:, None] & mm_d[None, :], + ) + + +def _fused_dropout_residual_fwd( + pair_2d: torch.Tensor, + delta_2d: torch.Tensor, + mask_2d: torch.Tensor, + n_row: int, + n_col: int, +) -> torch.Tensor: + M, D = pair_2d.shape + out = torch.empty_like(pair_2d) + BLOCK_M = 64 + BLOCK_D = min(128, _next_pow2(D)) + grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(D, BLOCK_D)) + _fused_dropout_residual_fwd_kernel[grid]( + pair_2d, + delta_2d, + mask_2d, + out, + M, + D, + n_col, + n_row * n_col, + BLOCK_M=BLOCK_M, + BLOCK_D=BLOCK_D, + num_warps=4, # pyright: ignore[reportCallIssue] + ) + return out + + +def _fused_dropout_residual_bwd( + dout_2d: torch.Tensor, mask_2d: torch.Tensor, n_row: int, n_col: int +) -> torch.Tensor: + M, D = dout_2d.shape + ddelta = torch.empty_like(dout_2d) + BLOCK_M = 64 + BLOCK_D = min(128, _next_pow2(D)) + grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(D, BLOCK_D)) + _fused_dropout_residual_bwd_kernel[grid]( + dout_2d, + mask_2d, + ddelta, + M, + D, + n_col, + n_row * n_col, + BLOCK_M=BLOCK_M, + BLOCK_D=BLOCK_D, + num_warps=4, # pyright: ignore[reportCallIssue] + ) + return ddelta + + +def _next_pow2(n: int) -> int: + p = 1 + while p < n: + p <<= 1 + return p + + +class FusedDropoutResidualFn(torch.autograd.Function): + """`pair_out = pair + delta * mask` fused into one kernel. + + Args: + pair: [B, N_row, N_col, D] + delta: [B, N_row, N_col, D] + mask: [B, 1, N_col, D] or [1, 1, N_col, D] — already scaled to 1/(1-r) for kept entries + + The mask is row-shared (size 1 along `N_row`) — we read it directly with + modular indexing rather than broadcasting and materializing a full [B, N, N, D] + copy. + """ + + @staticmethod + def forward(ctx, pair, delta, mask): + in_shape = pair.shape # [B, N_row, N_col, D] + N_row = in_shape[-3] + N_col = in_shape[-2] + D = in_shape[-1] + pair_2d = pair.contiguous().view(-1, D) + delta_2d = delta.contiguous().view(-1, D) + # [B, 1, N_col, D] → [B*N_col, D]; kernel indexes b*N_col + (m % N_col) + # so each batch sample uses its own draw. + mask_2d = mask.contiguous().view(-1, D) + out_2d = _fused_dropout_residual_fwd(pair_2d, delta_2d, mask_2d, N_row, N_col) + ctx.save_for_backward(mask_2d) + ctx.in_shape = in_shape + ctx.n_row = N_row + ctx.n_col = N_col + return out_2d.view(in_shape) + + @staticmethod + def backward(ctx, dout): + (mask_2d,) = ctx.saved_tensors + D = ctx.in_shape[-1] + dout_2d = dout.contiguous().view(-1, D) + ddelta_2d = _fused_dropout_residual_bwd(dout_2d, mask_2d, ctx.n_row, ctx.n_col) + # Residual: gradient passes through. Mask: not differentiable. + return dout, ddelta_2d.view(ctx.in_shape), None + + +class FusedDropoutResidual(nn.Module): + """Fused module for the pattern `pair + Dropout(r, batch_dim=1)(delta)`. + + The kernel only supports row-shared dropout — i.e. a `[B, 1, N_col, D]` + mask broadcast over the row axis (dim 1) — so this module hardcodes that + layout. The `j = m % N_col` indexing in the Triton kernel would read out + of bounds with any other sharing pattern, so we don't expose `batch_dim` + as a parameter. Use plain `Dropout(r, batch_dim=2)` for col-shared. + + Usage in a pairformer block: + + # Before + pair = pair + self.row_drop(self.tri_mul_out(pair, mask=...)) + + # After + pair = self.row_drop(pair, self.tri_mul_out(pair, mask=...)) + + where `self.row_drop` is `FusedDropoutResidual(r)`. + """ + + def __init__(self, r: float): + super().__init__() + self.r = r + + def forward(self, pair: torch.Tensor, delta: torch.Tensor) -> torch.Tensor: + if not self.training or self.r == 0.0: + return pair + delta + # Use delta.dtype: F.dropout's cuRAND draw depends on input dtype, so + # building from pair (fp32) vs delta (bf16 under autocast) breaks + # same-seed parity with the unfused `pair + Dropout(delta)` path. + shape = list(pair.shape) + shape[1] = 1 # row-shared mask: [B, 1, N_col, D] + ones = delta.new_ones(shape) + mask = torch.nn.functional.dropout(ones, p=self.r, training=True) + return FusedDropoutResidualFn.apply(pair, delta, mask) # type: ignore[return-value] diff --git a/src/transformers/models/esmfold2/kernels/fused_dual_gemm.py b/src/transformers/models/esmfold2/kernels/fused_dual_gemm.py new file mode 100644 index 000000000000..66ae88ec4208 --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/fused_dual_gemm.py @@ -0,0 +1,610 @@ +"""Native Triton ``fused_sigmoid_gated_dual_gemm`` for TriMul stage 2. + +Computes ``sigmoid(x @ w1.T) * (x @ w2.T)`` with optional row-shared mask, +bias, and transposed output. Forward + backward implemented in bf16. + +Inspired by cuequivariance's ``fused_sigmoid_gated_dual_gemm`` +(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently +re-implemented in Triton. +""" + +# ruff: noqa: E402 + +from __future__ import annotations + +import os +import warnings + +warnings.filterwarnings("ignore", category=FutureWarning) +warnings.filterwarnings("ignore", category=UserWarning) +warnings.filterwarnings("ignore", category=DeprecationWarning) +os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1") +os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1") + +import torch +import triton +import triton.language as tl + +# Static config — runtime autotune cold-start is unshippable for inference. +_AUTOTUNE_CONFIGS = [ + triton.Config( + {"TILE_M": 128, "TILE_N": 64, "TILE_K": 32, "GROUP_M": 8}, + num_stages=4, + num_warps=4, + ) +] + + +@triton.autotune( + configs=_AUTOTUNE_CONFIGS, key=["M", "N", "K", "HAS_MASK", "TRANSPOSE_OUT"] +) +@triton.jit +def _gated_dual_gemm_kernel( + x_ptr, # [M, K] bf16 + w1_ptr, # [N, K] bf16 — gate weight + w2_ptr, # [N, K] bf16 — value weight + mask_ptr, # [M] bf16 — row-shared mask broadcast to (M, N) + out_ptr, # [M, N] or [N, M] bf16 — sigmoid(x@w1) * (x@w2) + M, + N, + K, + TILE_M: tl.constexpr, + TILE_N: tl.constexpr, + TILE_K: tl.constexpr, + GROUP_M: tl.constexpr, + HAS_MASK: tl.constexpr, + TRANSPOSE_OUT: tl.constexpr, # store (N, M) instead of (M, N) + NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] +): + """Per (TILE_M, TILE_N) output tile: + gate_acc = Σ_K (x[:, k] @ w1[:, k]) over k + val_acc = Σ_K (x[:, k] @ w2[:, k]) over k + delta = sigmoid(gate_acc) * val_acc + if mask: delta *= mask[m_tile] (broadcast over N) + store delta (transposed if TRANSPOSE_OUT) + """ + pid_m_raw = tl.program_id(0) + pid_n_raw = tl.program_id(1) + + # GROUP_M swizzle for L2 reuse of ``x`` across consecutive CTAs. + num_pid_m = tl.cdiv(M, TILE_M) + num_pid_n = tl.cdiv(N, TILE_N) + pid = pid_n_raw * num_pid_m + pid_m_raw # row-major program id + num_pid_in_group = GROUP_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + if NEEDS_INT64: + pid_m = tl.cast(pid_m, tl.int64) + pid_n = tl.cast(pid_n, tl.int64) + M = tl.cast(M, tl.int64) + N = tl.cast(N, tl.int64) + K = tl.cast(K, tl.int64) + + start_m = pid_m * TILE_M + start_n = pid_n * TILE_N + + offs_m = start_m + tl.arange(0, TILE_M) + offs_n = start_n + tl.arange(0, TILE_N) + offs_k = tl.arange(0, TILE_K) + if NEEDS_INT64: + offs_m = tl.cast(offs_m, tl.int64) + offs_n = tl.cast(offs_n, tl.int64) + offs_k = tl.cast(offs_k, tl.int64) + + x_ptrs = x_ptr + (offs_m[:, None] * K + offs_k[None, :]) + w1_base = w1_ptr + (offs_n[None, :] * K + offs_k[:, None]) + w2_base = w2_ptr + (offs_n[None, :] * K + offs_k[:, None]) + + gate_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + val_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + mask_m = offs_m < M + + for _ in range(0, tl.cdiv(K, TILE_K)): + x_raw = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0) + x_op = x_raw.to(w1_ptr.type.element_ty) + w1_tile = tl.load(w1_base) + w2_tile = tl.load(w2_base) + gate_acc = tl.dot(x_op, w1_tile, gate_acc) + val_acc = tl.dot(x_op, w2_tile, val_acc) + x_ptrs += TILE_K + w1_base += TILE_K + w2_base += TILE_K + + delta = tl.sigmoid(gate_acc) * val_acc # fp32 + + if HAS_MASK: + mask_tile = tl.load(mask_ptr + offs_m, mask=mask_m, other=0.0).to(tl.float32) + delta = delta * mask_tile[:, None] + + if TRANSPOSE_OUT: + out_ptrs = out_ptr + (offs_n[:, None] * M + offs_m[None, :]) + tl.store( + out_ptrs, tl.trans(delta).to(out_ptr.type.element_ty), mask=mask_m[None, :] + ) + else: + out_ptrs = out_ptr + (offs_m[:, None] * N + offs_n[None, :]) + tl.store(out_ptrs, delta.to(out_ptr.type.element_ty), mask=mask_m[:, None]) + + +# Backward emits per-element grad_gate_logits and grad_val_acc by recomputing +# the two forward GEMMs. Weight grads (d_w1, d_w2, d_x) are done in cuBLAS in +# the autograd Function — cuBLAS beats Triton bf16 at these reduction shapes. +# Narrower TILE_N=64 vs forward's 128 (bwd writes two M*N tensors, doubling +# register pressure). +_BWD_AUTOTUNE_CONFIGS = [ + triton.Config( + {"TILE_M": 64, "TILE_N": 64, "TILE_K": 64, "GROUP_M": 8}, + num_stages=3, + num_warps=4, + ) +] + + +@triton.autotune( + configs=_BWD_AUTOTUNE_CONFIGS, + key=["M", "N", "K", "HAS_MASK", "GRAD_OUT_TRANSPOSED", "GRAD_OUT_SPLIT"], +) +@triton.jit +def _gated_dual_gemm_backward_kernel( + grad_out_ptr, # [M, N] (or [N, M] if GRAD_OUT_TRANSPOSED, or [N/2, M] if GRAD_OUT_SPLIT) + grad_out2_ptr, # GRAD_OUT_SPLIT only: second half of (N, M); else dummy + x_ptr, # [M, K] bf16 — saved input + w1_ptr, # [N, K] bf16 + w2_ptr, # [N, K] bf16 + mask_ptr, # [M] bf16 — row-shared (unused if HAS_MASK=0) + grad_gate_logits_ptr, # [M, N] bf16 — out: d (x @ w1.T) + grad_val_acc_ptr, # [M, N] bf16 — out: d (x @ w2.T) + grad_mask_partials_ptr, # [num_pid_n, M] fp32 — partial mask grads + M, + N, + K, + HALF_N: tl.constexpr, # = N // 2, only used when GRAD_OUT_SPLIT=1 + TILE_M: tl.constexpr, + TILE_N: tl.constexpr, + TILE_K: tl.constexpr, + GROUP_M: tl.constexpr, + HAS_MASK: tl.constexpr, + GRAD_OUT_TRANSPOSED: tl.constexpr, # 1: load grad from (N, M) layout + GRAD_OUT_SPLIT: tl.constexpr, # 1: read from two (N/2, M) tensors (chunk-free path) + NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] +): + """Per (TILE_M, TILE_N) output tile: + gate_acc = Σ_K x[:, k] @ w1[:, k] + val_acc = Σ_K x[:, k] @ w2[:, k] + g = sigmoid(gate_acc) + grad_o = grad_out_tile (post-mask: multiply by mask if HAS_MASK) + d_gate_logits = grad_o * val_acc * g * (1 - g) + d_val_acc = grad_o * g + d_mask_partial[pid_n, m] = sum_n (grad_out_tile * g * val_acc) [if HAS_MASK] + """ + pid_m_raw = tl.program_id(axis=0) + pid_n_raw = tl.program_id(axis=1) + + num_pid_m = tl.cdiv(M, TILE_M) + num_pid_n = tl.cdiv(N, TILE_N) + pid = pid_n_raw * num_pid_m + pid_m_raw + num_pid_in_group = GROUP_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + if NEEDS_INT64: + pid_m = tl.cast(pid_m, tl.int64) + pid_n = tl.cast(pid_n, tl.int64) + M = tl.cast(M, tl.int64) + N = tl.cast(N, tl.int64) + K = tl.cast(K, tl.int64) + + start_m = pid_m * TILE_M + start_n = pid_n * TILE_N + + offs_m = start_m + tl.arange(0, TILE_M) + offs_n = start_n + tl.arange(0, TILE_N) + offs_k = tl.arange(0, TILE_K) + if NEEDS_INT64: + offs_m = tl.cast(offs_m, tl.int64) + offs_n = tl.cast(offs_n, tl.int64) + offs_k = tl.cast(offs_k, tl.int64) + + x_ptrs = x_ptr + (offs_m[:, None] * K + offs_k[None, :]) + w1_base = w1_ptr + (offs_n[None, :] * K + offs_k[:, None]) + w2_base = w2_ptr + (offs_n[None, :] * K + offs_k[:, None]) + + gate_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + val_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + mask_m = offs_m < M + + # Recompute fwd GEMMs (cheaper than saving N*M activations). + for _ in range(0, tl.cdiv(K, TILE_K)): + x_tile = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0) + x_op = x_tile.to(w1_ptr.type.element_ty) + w1_tile = tl.load(w1_base) + w2_tile = tl.load(w2_base) + gate_acc = tl.dot(x_op, w1_tile, gate_acc) + val_acc = tl.dot(x_op, w2_tile, val_acc) + x_ptrs += TILE_K + w1_base += TILE_K + w2_base += TILE_K + + g = tl.sigmoid(gate_acc) + + if GRAD_OUT_SPLIT: + # Two (N/2, M) grad tensors; caller guarantees TILE_N divides HALF_N. + if start_n < HALF_N: + offs_n_local = offs_n + grad_o_ptrs = grad_out_ptr + (offs_n_local[None, :] * M + offs_m[:, None]) + else: + offs_n_local = offs_n - HALF_N + grad_o_ptrs = grad_out2_ptr + (offs_n_local[None, :] * M + offs_m[:, None]) + elif GRAD_OUT_TRANSPOSED: + grad_o_ptrs = grad_out_ptr + (offs_n[None, :] * M + offs_m[:, None]) + else: + grad_o_ptrs = grad_out_ptr + (offs_m[:, None] * N + offs_n[None, :]) + grad_o = tl.load(grad_o_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + + if HAS_MASK: + # d_mask = row-sum BEFORE mask multiply; per-pid_n partial, host reduces. + d_mask_partial = tl.sum(grad_o * g * val_acc, axis=1) + d_mask_partials_ptrs = grad_mask_partials_ptr + pid_n * M + offs_m + tl.store(d_mask_partials_ptrs, d_mask_partial, mask=mask_m) + mask_tile = tl.load(mask_ptr + offs_m, mask=mask_m, other=0.0).to(tl.float32) + grad_o = grad_o * mask_tile[:, None] + + # Both grad ptrs index into one (M, 2N) buffer (val_acc cols [0:N], + # gate_logits cols [N:2N]) — lets downstream d_x / d_w fold into single GEMMs. + d_val_acc = (grad_o * g).to(grad_val_acc_ptr.type.element_ty) + d_val_acc_ptrs = grad_val_acc_ptr + (offs_m[:, None] * (2 * N) + offs_n[None, :]) + tl.store(d_val_acc_ptrs, d_val_acc, mask=mask_m[:, None]) + + d_gate_logits = (grad_o * val_acc * g * (1.0 - g)).to( + grad_gate_logits_ptr.type.element_ty + ) + d_gate_logits_ptrs = grad_gate_logits_ptr + ( + offs_m[:, None] * (2 * N) + offs_n[None, :] + ) + tl.store(d_gate_logits_ptrs, d_gate_logits, mask=mask_m[:, None]) + + +# Backward TILE_N must match the kernel's autotuned tile in ``_BWD_AUTOTUNE_CONFIGS``; +# host code uses it to size the per-tile mask-grad partials buffer statically. +_BWD_TILE_N = 64 + + +def _fused_gated_dual_gemm_bwd( + grad_out: torch.Tensor, # (M, N) or (N, M) layout (see grad_out_transposed) + x: torch.Tensor, # [..., K] (un-flattened, original) + w1: torch.Tensor, + w2: torch.Tensor, + mask: torch.Tensor | None, + grad_out_transposed: bool = False, + grad_out_split: tuple[torch.Tensor, torch.Tensor] | None = None, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]: + """Compute (d_x, d_w1, d_w2, d_mask) for ``fused_gated_dual_gemm``. + + Three grad_out layouts supported: + * default: ``grad_out`` of shape ``(..., N)`` flattened to (M, N). + * ``grad_out_transposed=True``: shape ``(N, ...)`` flattened to (N, M). + * ``grad_out_split=(g1, g2)``: two tensors, each of shape ``(N/2, ...)`` + flattened to ``(N/2, M)``; the kernel reads from the appropriate + pointer per tile. Avoids the autograd-introduced concat that + ``torch.chunk(dim=0)``'s backward inserts (saves 3-4 ms at prod + shape, B=5 L=768 c_z=128). + """ + in_shape = x.shape + K = in_shape[-1] + x_c = x if x.is_contiguous() else x.contiguous() + x_2d = x_c.view(-1, K) + M = x_2d.shape[0] + N = w1.shape[0] + + assert ( + w1.dtype == w2.dtype == torch.bfloat16 + ), f"weights must be bf16; got {w1.dtype}/{w2.dtype}" + assert x_2d.dtype == torch.bfloat16, "bwd only supports bf16 x" + + if grad_out_split is not None: + g1, g2 = grad_out_split + half_n = N // 2 + # Materialize only if non-contig (avoids a full-tensor copy). + if not g1.is_contiguous(): + g1 = g1.contiguous() + if not g2.is_contiguous(): + g2 = g2.contiguous() + assert g1.numel() == half_n * M and g2.numel() == half_n * M, ( + f"split grad sizes mismatch: g1={g1.numel()} g2={g2.numel()} " + f"vs half_n*M={half_n * M}" + ) + grad_out_2d_a = g1.view(half_n, M) + grad_out_2d_b = g2.view(half_n, M) + elif grad_out_transposed: + grad_out_2d_a = grad_out.contiguous().view(N, M) + grad_out_2d_b = grad_out_2d_a # unused (dummy) + else: + grad_out_2d_a = grad_out.contiguous().view(M, N) + grad_out_2d_b = grad_out_2d_a # unused (dummy) + + mask_flat = mask.contiguous().view(-1) if mask is not None else None + if mask_flat is not None: + assert mask_flat.shape[0] == M + + # (M, 2N) combined-grad buffer; matched stacked_w order (w2 then w1) + # lets d_w and d_x each collapse to a single cuBLAS GEMM. + grad_combined = torch.empty((M, 2 * N), device=x.device, dtype=torch.bfloat16) + grad_val_acc = grad_combined[:, :N] + grad_gate_logits = grad_combined[:, N:] + + tiles_n_max = triton.cdiv(N, _BWD_TILE_N) + if mask is not None: + grad_mask_partials = torch.empty( + (tiles_n_max, M), device=x.device, dtype=torch.float32 + ) + else: + grad_mask_partials = torch.empty((1,), device=x.device, dtype=torch.float32) + + _dummy_mask = torch.zeros((), device=x.device, dtype=torch.bfloat16) + + # (M, 2N) layout → use stride 2N for the int32-overflow gate. + NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * (2 * N) >= 2**31 - 1) + + def grid(meta): + assert N % meta["TILE_N"] == 0 + return (triton.cdiv(M, meta["TILE_M"]), N // meta["TILE_N"]) + + half_n = N // 2 if grad_out_split is not None else 1 + + _gated_dual_gemm_backward_kernel[grid]( + grad_out_2d_a, + grad_out_2d_b, + x_2d, + w1.contiguous(), + w2.contiguous(), + mask_flat if mask_flat is not None else _dummy_mask, + grad_gate_logits, + grad_val_acc, + grad_mask_partials, + M, + N, + K, + HALF_N=half_n, + HAS_MASK=mask is not None, + GRAD_OUT_TRANSPOSED=grad_out_transposed, + GRAD_OUT_SPLIT=grad_out_split is not None, + NEEDS_INT64=NEEDS_INT64, + ) + + # Stacked weights for the single-GEMM d_x path. + stacked_w = torch.cat([w2, w1], dim=0) # (2N, K), matches val|gate layout + + d_x_2d = grad_combined @ stacked_w + d_x = d_x_2d.view(in_shape) + + d_w_combined = grad_combined.t() @ x_2d # (2N, K) + d_w2 = d_w_combined[:N] + d_w1 = d_w_combined[N:] + + if mask is not None: + actual_tiles = triton.cdiv(N, _BWD_TILE_N) + d_mask_flat = grad_mask_partials[:actual_tiles].sum(dim=0).to(mask.dtype) + d_mask = d_mask_flat.view(mask.shape) + else: + d_mask = None + + return d_x, d_w1, d_w2, d_mask + + +class FusedGatedDualGEMM(torch.autograd.Function): + """Autograd wrapper for ``fused_gated_dual_gemm`` (bf16 path, + ``transpose_out=False``). + + Saves ``x``, ``w1``, ``w2``, ``mask`` for the backward kernel; the + backward kernel recomputes the two dual-GEMM activations (cheap vs + materializing them in fwd context). + """ + + @staticmethod + def forward( + ctx, + x: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + mask: torch.Tensor | None, + ) -> torch.Tensor: + out = _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=False) + if mask is None: + ctx.save_for_backward(x, w1, w2) + ctx.has_mask = False + else: + ctx.save_for_backward(x, w1, w2, mask) + ctx.has_mask = True + return out + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] + if ctx.has_mask: + x, w1, w2, mask = ctx.saved_tensors + else: + x, w1, w2 = ctx.saved_tensors + mask = None + if not grad_out.is_contiguous(): + grad_out = grad_out.contiguous() + d_x, d_w1, d_w2, d_mask = _fused_gated_dual_gemm_bwd(grad_out, x, w1, w2, mask) + return d_x, d_w1, d_w2, d_mask + + +class FusedGatedDualGEMMSplit(torch.autograd.Function): + """Autograd wrapper that returns the dual-GEMM output as two split halves. + + Forward: runs the kernel with ``transpose_out=True``, producing a single + ``(N=2*c_z, *trailing)`` buffer where the gate half (first c_z) and the + value half (next c_z) live contiguously along dim 0. Returns the two + views ``(a, b_t)`` so downstream einsums consume them in + ``(c_z, B, L, L)`` layout — no chunk in the autograd graph. + + Backward: receives ``(grad_a, grad_b_t)`` (each ``(c_z, *trailing)``) + and passes them as the ``grad_out_split`` pair to the backward kernel. + Eliminates the ~3.5 ms ``torch.chunk(dim=0)`` backward concat at the + prod shape (B=5, L=768, c_z=128) by reading the two halves from + separate pointers inside the kernel. + """ + + @staticmethod + def forward( + ctx, + x: torch.Tensor, # (B, L, L, c_z) bf16 + w1: torch.Tensor, # (N=2*c_z, c_z) bf16 — gate + w2: torch.Tensor, # (N=2*c_z, c_z) bf16 — value + mask: torch.Tensor | None, # (B, L, L) bf16 or None + trailing_shape: tuple, # (B, L, L) for output view + ) -> tuple[torch.Tensor, torch.Tensor]: + out = _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=True) + # Slicing on a leading dim of a contiguous tensor stays contiguous (no copy). + N = w1.shape[0] + half_n = N // 2 + out_view = out.view((N,) + trailing_shape) + a = out_view[:half_n] + b_t = out_view[half_n:] + if mask is None: + ctx.save_for_backward(x, w1, w2) + ctx.has_mask = False + else: + ctx.save_for_backward(x, w1, w2, mask) + ctx.has_mask = True + return a, b_t + + @staticmethod + def backward(ctx, grad_a: torch.Tensor, grad_b_t: torch.Tensor): # type: ignore[override] + if ctx.has_mask: + x, w1, w2, mask = ctx.saved_tensors + else: + x, w1, w2 = ctx.saved_tensors + mask = None + # Don't call .contiguous() — _fused_gated_dual_gemm_bwd validates instead + # (avoids a full-tensor copy on the common contig path). + d_x, d_w1, d_w2, d_mask = _fused_gated_dual_gemm_bwd( + None, # type: ignore[arg-type] + x, + w1, + w2, + mask, + grad_out_split=(grad_a, grad_b_t), + ) + return d_x, d_w1, d_w2, d_mask, None + + +def fused_gated_dual_gemm_split( + x: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + mask: torch.Tensor | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Split-output variant of ``fused_gated_dual_gemm``: returns ``(a, b_t)`` + with layout ``(c_z, B, L, L)`` each, avoiding chunk in the autograd graph. + + Inference fallback (no grad) just calls the regular fwd with + ``transpose_out=True`` and chunks the result — same layout, no kernel + change required. + """ + trailing_shape = tuple(x.shape[:-1]) + if torch.is_grad_enabled() and ( + x.requires_grad or w1.requires_grad or w2.requires_grad + ): + return FusedGatedDualGEMMSplit.apply(x, w1, w2, mask, trailing_shape) # type: ignore[return-value] + out = _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=True) + N = w1.shape[0] + out_view = out.view((N,) + trailing_shape) + half_n = N // 2 + return out_view[:half_n], out_view[half_n:] + + +def _fused_gated_dual_gemm_fwd( + x: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + mask: torch.Tensor | None = None, + transpose_out: bool = False, +) -> torch.Tensor: + """Native Triton implementation of sigmoid-gated dual GEMM. + + Computes ``sigmoid(x @ w1.T) * (x @ w2.T)`` with optional row-shared mask. + + Shapes: + x: (..., K) bf16 + w1: (N, K) bf16 + w2: (N, K) bf16 + mask: (...,) bf16 — flattened to a per-row scalar + out: (..., N) or (N, ...) when ``transpose_out=True`` + """ + in_shape = x.shape + K = in_shape[-1] + x_2d = x.contiguous().view(-1, K) + M = x_2d.shape[0] + N = w1.shape[0] + + assert w1.shape == w2.shape, f"w1 {w1.shape} ≠ w2 {w2.shape}" + assert w1.shape[1] == K + assert ( + w1.dtype == w2.dtype == torch.bfloat16 + ), f"weights must be bf16; got {w1.dtype}/{w2.dtype}" + + out_dtype = torch.bfloat16 + if transpose_out: + out = torch.empty((N, M), device=x.device, dtype=out_dtype) + out_shape = (N,) + in_shape[:-1] + else: + out = torch.empty((M, N), device=x.device, dtype=out_dtype) + out_shape = in_shape[:-1] + (N,) + + mask_flat = mask.contiguous().view(-1) if mask is not None else None + if mask_flat is not None: + assert mask_flat.shape[0] == M, f"mask len {mask_flat.shape[0]} ≠ M {M}" + + _dummy = torch.zeros((), device=x.device, dtype=torch.float32) + + NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * N >= 2**31 - 1) + + def grid(meta): + assert N % meta["TILE_N"] == 0 + return (triton.cdiv(M, meta["TILE_M"]), N // meta["TILE_N"]) + + _gated_dual_gemm_kernel[grid]( + x_2d, + w1.contiguous(), + w2.contiguous(), + mask_flat if mask_flat is not None else _dummy, + out, + M, + N, + K, + HAS_MASK=mask is not None, + TRANSPOSE_OUT=transpose_out, + NEEDS_INT64=NEEDS_INT64, + ) + return out.view(out_shape) + + +def fused_gated_dual_gemm( + x: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + mask: torch.Tensor | None = None, + transpose_out: bool = False, +) -> torch.Tensor: + """Public wrapper: dispatches to autograd Function if grad is enabled. + + ``transpose_out=True`` is inference-only (bypasses autograd). + """ + if torch.is_grad_enabled() and ( + x.requires_grad or w1.requires_grad or w2.requires_grad + ): + assert not transpose_out, ( + "transpose_out=True is inference-only; train path must use the " + "non-transposed output (post-stage-3 einsum already handles layout)" + ) + return FusedGatedDualGEMM.apply(x, w1, w2, mask) # type: ignore[return-value] + return _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=transpose_out) diff --git a/src/transformers/models/esmfold2/kernels/fused_ln_residual.py b/src/transformers/models/esmfold2/kernels/fused_ln_residual.py new file mode 100644 index 000000000000..ec349e64a830 --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/fused_ln_residual.py @@ -0,0 +1,397 @@ +"""Triton LayerNorm (bf16 IO, fp32 stats) with optional fused residual-add in +the *backward* pass. + +Inspired by cuequivariance's ``layer_norm_transpose`` +(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently +re-implemented in Triton to add the bwd residual-link fusion. + +Used by ``trimul_with_residual.triangle_multiplicative_update_with_residual`` +for two LN calls: + + * Stage 1 LN: ``x_in = LN(pair)`` with layout ``bijd->bijd``. The downstream + residual add ``out = residual + delta`` (where ``residual = pair``) flows + ``grad_out`` straight back to ``grad_pair``. Fusing that add into LN's + bwd kernel saves one full pair-tensor read + one full write (~250 MB at + B=5 L=768 c_z=128 bf16). Exposed via :func:`fused_ln_with_residual_link`. + + * Stage 4 LN: ``x_out = LN(einsum(...))`` with layout ``dbij->bijd``. No + residual to fuse — :func:`fused_ln_transpose` is the plain variant. + +The residual-link fusion in the bwd pass is the motivation for shipping this: +the bwd kernel computes ``grad_x = LN_bwd(grad_y, x, w, mean, rstd)`` AND +optionally adds an external ``grad_residual`` tensor into ``grad_x`` in the +same pass — saves one HBM round-trip of the (M, D) tensor. + +Forward + backward use a single static ``triton.Config`` each (runtime +autotune cold-start is unshippable for inference paths). The bwd pass +emits per-tile ``grad_w``/``grad_b`` fp32 partials reduced host-side; this +is the standard Triton LN-bwd idiom (see openai/triton tutorial 05). +""" + +import torch +import triton +import triton.language as tl + +# Layout enum: 0 == "bijd->bijd" (bnd contig), 1 == "dbij->bijd" (dbn contig in). +_LAYOUT_BND_BND = 0 +_LAYOUT_DBN_BND = 1 + +_FWD_TILE_M = 64 +_FWD_NUM_WARPS = 8 +_FWD_NUM_STAGES = 2 + +_BWD_TILE_M = 64 +_BWD_NUM_WARPS = 8 +_BWD_NUM_STAGES = 2 + + +@triton.jit +def _ln_fwd_kernel( + x_ptr, # input + w_ptr, # [D] + b_ptr, # [D] + out_ptr, # [M, D] contig output + mean_ptr, # [M] fp32 + rstd_ptr, # [M] fp32 + M, + D: tl.constexpr, + EPS: tl.constexpr, + LAYOUT: tl.constexpr, + TILE_M: tl.constexpr, +): + pid = tl.program_id(axis=0).to(tl.int64) + M64 = M.to(tl.int64) + + offs_m = pid * TILE_M + tl.arange(0, TILE_M).to(tl.int64) + offs_d = tl.arange(0, D).to(tl.int64) + mask_m = offs_m < M64 + + if LAYOUT == 0: + x_ptrs = x_ptr + offs_m[:, None] * D + offs_d[None, :] + x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + else: # LAYOUT == 1: (D, M) contig + x_ptrs = x_ptr + offs_d[None, :] * M64 + offs_m[:, None] + x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + + mean = tl.sum(x, axis=1) / D + x_c = x - mean[:, None] + var = tl.sum(x_c * x_c, axis=1) / D + rstd = 1.0 / tl.sqrt(var + EPS) + x_hat = x_c * rstd[:, None] + + tl.store(mean_ptr + offs_m, mean, mask=mask_m) + tl.store(rstd_ptr + offs_m, rstd, mask=mask_m) + + w = tl.load(w_ptr + offs_d).to(tl.float32) + b = tl.load(b_ptr + offs_d).to(tl.float32) + y = x_hat * w[None, :] + b[None, :] + + out_ptrs = out_ptr + offs_m[:, None] * D + offs_d[None, :] + tl.store(out_ptrs, y.to(out_ptr.type.element_ty), mask=mask_m[:, None]) + + +@triton.jit +def _ln_bwd_kernel( + grad_y_ptr, # [M, D] (always bnd; LN out is bnd) + x_ptr, # input — layout LAYOUT + w_ptr, # [D] + mean_ptr, # [M] fp32 + rstd_ptr, # [M] fp32 + grad_x_ptr, # output — layout LAYOUT (same as x) + grad_w_partial_ptr, # [num_tiles, D] fp32 + grad_b_partial_ptr, # [num_tiles, D] fp32 + grad_residual_ptr, # [M, D] in bnd layout; ignored if HAS_RESIDUAL=0 + M, + D: tl.constexpr, + LAYOUT: tl.constexpr, + HAS_RESIDUAL: tl.constexpr, + TILE_M: tl.constexpr, +): + pid = tl.program_id(axis=0).to(tl.int64) + M64 = M.to(tl.int64) + + offs_m = pid * TILE_M + tl.arange(0, TILE_M).to(tl.int64) + offs_d = tl.arange(0, D).to(tl.int64) + mask_m = offs_m < M64 + + grad_y_ptrs = grad_y_ptr + offs_m[:, None] * D + offs_d[None, :] + grad_y = tl.load(grad_y_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + + if LAYOUT == 0: + x_ptrs = x_ptr + offs_m[:, None] * D + offs_d[None, :] + x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + else: + x_ptrs = x_ptr + offs_d[None, :] * M64 + offs_m[:, None] + x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + + mean = tl.load(mean_ptr + offs_m, mask=mask_m, other=0.0) + rstd = tl.load(rstd_ptr + offs_m, mask=mask_m, other=0.0) + w = tl.load(w_ptr + offs_d).to(tl.float32) + + x_hat = (x - mean[:, None]) * rstd[:, None] + wdy = grad_y * w[None, :] + c1 = tl.sum(wdy, axis=1) / D + c2 = tl.sum(wdy * x_hat, axis=1) / D + grad_x = (wdy - (c1[:, None] + x_hat * c2[:, None])) * rstd[:, None] + + if HAS_RESIDUAL: + gr_ptrs = grad_residual_ptr + offs_m[:, None] * D + offs_d[None, :] + gr = tl.load(gr_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + grad_x = grad_x + gr + + if LAYOUT == 0: + gx_ptrs = grad_x_ptr + offs_m[:, None] * D + offs_d[None, :] + tl.store(gx_ptrs, grad_x.to(grad_x_ptr.type.element_ty), mask=mask_m[:, None]) + else: + gx_ptrs = grad_x_ptr + offs_d[None, :] * M64 + offs_m[:, None] + tl.store(gx_ptrs, grad_x.to(grad_x_ptr.type.element_ty), mask=mask_m[:, None]) + + # Per-tile partial reduction → write to (num_tiles, D) fp32 buffer. + # Final reduction (sum over num_tiles) happens host-side as a torch.sum + # (standard Triton LN-bwd idiom; see openai/triton tutorial 05). + mask_f = mask_m[:, None].to(tl.float32) + dw_tile = tl.sum(grad_y * x_hat * mask_f, axis=0) + db_tile = tl.sum(grad_y * mask_f, axis=0) + dw_offs = pid * D + offs_d + db_offs = pid * D + offs_d + tl.store(grad_w_partial_ptr + dw_offs, dw_tile) + tl.store(grad_b_partial_ptr + db_offs, db_tile) + + +def _layout_to_int(layout: str) -> int: + if layout in ("bijd->bijd", "bnd->bnd"): + return _LAYOUT_BND_BND + if layout in ("dbij->bijd", "dbn->bnd"): + return _LAYOUT_DBN_BND + raise ValueError(f"unsupported layout {layout!r}") + + +def _reshape_for_layout( + x: torch.Tensor, layout: str +) -> tuple[tuple[int, ...], int, int, torch.Tensor]: + """Reshape x to 2D LN view. Returns (out_shape, M, D, x_view).""" + if layout == "bijd->bijd": + B, II, J, D = x.shape + M = B * II * J + return (B, II, J, D), M, D, x.contiguous().view(M, D) + if layout == "bnd->bnd": + B, N, D = x.shape + M = B * N + return (B, N, D), M, D, x.contiguous().view(M, D) + if layout == "dbij->bijd": + D, B, II, J = x.shape + M = B * II * J + return (B, II, J, D), M, D, x.contiguous().view(D, M) + if layout == "dbn->bnd": + D, B, N = x.shape + M = B * N + return (B, N, D), M, D, x.contiguous().view(D, M) + raise ValueError(f"unsupported layout {layout!r}") + + +def _ln_fwd( + x_view: torch.Tensor, + w: torch.Tensor, + b: torch.Tensor, + eps: float, + layout_int: int, + M: int, + D: int, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + out = torch.empty((M, D), device=x_view.device, dtype=x_view.dtype) + mean = torch.empty((M,), device=x_view.device, dtype=torch.float32) + rstd = torch.empty((M,), device=x_view.device, dtype=torch.float32) + grid = (triton.cdiv(M, _FWD_TILE_M),) + _ln_fwd_kernel[grid]( + x_view, + w, + b, + out, + mean, + rstd, + M, + D=D, + EPS=eps, + LAYOUT=layout_int, + TILE_M=_FWD_TILE_M, + num_warps=_FWD_NUM_WARPS, # type: ignore[call-arg] + num_stages=_FWD_NUM_STAGES, # type: ignore[call-arg] + ) + return out, mean, rstd + + +def _ln_bwd( + grad_y: torch.Tensor, + x_view: torch.Tensor, + w: torch.Tensor, + mean: torch.Tensor, + rstd: torch.Tensor, + layout_int: int, + grad_residual: torch.Tensor | None, + M: int, + D: int, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + if layout_int == _LAYOUT_BND_BND: + grad_x = torch.empty((M, D), device=x_view.device, dtype=x_view.dtype) + else: + grad_x = torch.empty((D, M), device=x_view.device, dtype=x_view.dtype) + + num_tiles = triton.cdiv(M, _BWD_TILE_M) + grad_w_partial = torch.empty( + (num_tiles, D), device=x_view.device, dtype=torch.float32 + ) + grad_b_partial = torch.empty( + (num_tiles, D), device=x_view.device, dtype=torch.float32 + ) + + has_residual = grad_residual is not None + _dummy = torch.empty((), device=x_view.device, dtype=grad_y.dtype) + grid = (num_tiles,) + _ln_bwd_kernel[grid]( + grad_y, + x_view, + w, + mean, + rstd, + grad_x, + grad_w_partial, + grad_b_partial, + grad_residual if has_residual else _dummy, + M, + D=D, + LAYOUT=layout_int, + HAS_RESIDUAL=has_residual, + TILE_M=_BWD_TILE_M, + num_warps=_BWD_NUM_WARPS, # type: ignore[call-arg] + num_stages=_BWD_NUM_STAGES, # type: ignore[call-arg] + ) + + grad_w = grad_w_partial.sum(dim=0).to(w.dtype) + grad_b = grad_b_partial.sum(dim=0).to(w.dtype) + return grad_x, grad_w, grad_b + + +class _LayerNormTransposeFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, x: torch.Tensor, w: torch.Tensor, b: torch.Tensor, eps: float, layout: str + ) -> torch.Tensor: + layout_int = _layout_to_int(layout) + out_shape, M, D, x_view = _reshape_for_layout(x, layout) + out_bnd, mean, rstd = _ln_fwd(x_view, w, b, eps, layout_int, M, D) + ctx.save_for_backward(x_view, w, mean, rstd) + ctx.layout_int = layout_int + ctx.M = M + ctx.D = D + ctx.x_orig_shape = x.shape + return out_bnd.view(*out_shape) + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] + x_view, w, mean, rstd = ctx.saved_tensors + grad_y = grad_out.contiguous().view(ctx.M, ctx.D) + grad_x, grad_w, grad_b = _ln_bwd( + grad_y, x_view, w, mean, rstd, ctx.layout_int, None, ctx.M, ctx.D + ) + grad_x = grad_x.view(*ctx.x_orig_shape) + return grad_x, grad_w, grad_b, None, None + + +def fused_ln_transpose( + x: torch.Tensor, + w: torch.Tensor, + b: torch.Tensor, + eps: float = 1e-5, + layout: str = "bijd->bijd", +) -> torch.Tensor: + """Plain LN replacement for ``layer_norm_transpose`` (no residual fusion).""" + return _LayerNormTransposeFn.apply(x, w, b, eps, layout) # type: ignore[return-value] + + +# Stage-1 LN with residual-add folded into the bwd kernel. The Function returns +# (ln_out, residual_alias); downstream uses ln_out, stage-5 uses residual_alias +# as the residual input. In bwd we receive both grad tensors and fold +# grad_residual_alias into grad_x in-kernel (saves one HBM round-trip). +# residual_alias is a fresh tensor (not a view) so autograd reliably routes +# its grad back through this Function. + + +class _LayerNormWithResidualLinkFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: torch.Tensor, + w: torch.Tensor, + b: torch.Tensor, + residual_link: torch.Tensor, + eps: float, + layout: str, + ) -> tuple[torch.Tensor, torch.Tensor]: + layout_int = _layout_to_int(layout) + out_shape, M, D, x_view = _reshape_for_layout(x, layout) + out_bnd, mean, rstd = _ln_fwd(x_view, w, b, eps, layout_int, M, D) + ctx.save_for_backward(x_view, w, mean, rstd) + ctx.layout_int = layout_int + ctx.M = M + ctx.D = D + ctx.x_orig_shape = x.shape + ctx.residual_link_shape = residual_link.shape + + ln_out = out_bnd.view(*out_shape) + # residual_alias: returning ``residual_link`` itself works in + # custom Functions — autograd treats the input-as-output case + # correctly (sums grads back into the input). The downstream + # graph node ``out = residual_alias + delta`` sees a regular + # tensor and produces grad of shape == residual_link. + # Note: we use .view_as for safety so the AutogradMeta is fresh. + return ln_out, residual_link.view_as(residual_link) + + @staticmethod + def backward(ctx, grad_ln_out: torch.Tensor, grad_link_pass: torch.Tensor): # type: ignore[override] + x_view, w, mean, rstd = ctx.saved_tensors + grad_y = grad_ln_out.contiguous().view(ctx.M, ctx.D) + + # grad_link_pass shape == residual_link shape. + if grad_link_pass is None: + grad_residual = None + else: + grad_residual = grad_link_pass.contiguous().view(ctx.M, ctx.D) + + grad_x, grad_w, grad_b = _ln_bwd( + grad_y, x_view, w, mean, rstd, ctx.layout_int, grad_residual, ctx.M, ctx.D + ) + grad_x = grad_x.view(*ctx.x_orig_shape) + + # We folded grad_link_pass into grad_x → return None for that input's + # grad slot, since we've already accounted for it via x's grad path + # (the caller is expected to pass x == residual_link, so grad_x += + # grad_residual is exactly the combined grad on the shared leaf). + # If caller passes DIFFERENT tensors for x and residual_link, the + # link's grad is *lost* — that's a usage error we accept by contract. + return grad_x, grad_w, grad_b, None, None, None + + +def fused_ln_with_residual_link( + x: torch.Tensor, + w: torch.Tensor, + b: torch.Tensor, + residual_link: torch.Tensor, + eps: float = 1e-5, + layout: str = "bijd->bijd", +) -> tuple[torch.Tensor, torch.Tensor]: + """LN(x) with a residual-link passthrough that fuses + ``grad_residual_link`` into the LN backward kernel. + + Contract: ``x`` and ``residual_link`` MUST refer to the same leaf + tensor (same identity). The returned ``residual_alias`` MUST be used as + the residual input to the downstream add — otherwise the fusion routes + the grad incorrectly. + + Returns ``(ln_out, residual_alias)``. + """ + if x is not residual_link: + raise ValueError( + "fused_ln_with_residual_link requires x and residual_link to be the " + "same tensor instance (the caller must wire pair → both)." + ) + return _LayerNormWithResidualLinkFn.apply(x, w, b, residual_link, eps, layout) # type: ignore[return-value] diff --git a/src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py b/src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py new file mode 100644 index 000000000000..ff507d84fee5 --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py @@ -0,0 +1,415 @@ +"""Fused LayerNorm + Linear(d, 2*d_inner) + SwiGLU(silu(x1) * x2) kernel. + +Collapses the standard LayerNorm -> Linear -> chunk -> SiLU -> mul sequence +into a single Triton kernel: + + out[..., d_inner] = silu(x1) * x2 where (x1, x2) = chunk(LN(x) @ W12, 2) + +Compared to the unfused PyTorch sequence this kernel: + + 1. Eliminates the [M, 2*d_inner] HBM read between Linear and SwiGLU. + 2. Halves X reads in the matmul: each program produces both halves of the + linear output (against W12_a and W12_b) in one pass over X. + 3. Fuses LayerNorm into the matmul k-loop. + +The output ordering uses silu of the FIRST half of W12's output, so the +fused module matches the standard LN+SwiGLU MLP composition. + +Backward: SwiGLU bwd is a small Triton kernel that mutates the saved [M, 2N] +linear-output buffer in place to produce dlin (no extra alloc). LN+Linear bwd +uses cuBLAS gemm + ATen's `native_layer_norm_backward` — fast on H100 and +avoids needing per-shape autotuned Triton bwd configs. + +Tuned forward configs were autotuned on H100 80GB for the pair-transition +production shapes (d_pair=256, d_inner=1024, M=B*N*N at N=384 / 640). +""" + +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F +import triton +import triton.language as tl + + +@triton.jit +def _ln_stats_kernel( + X_ptr, + X_row_stride, + Mean_ptr, + Mean_row_stride, + Rstd_ptr, + Rstd_row_stride, + K, + eps, + BLOCK_SIZE: tl.constexpr, +): + """Per-row LayerNorm reduction: writes mean and 1/sqrt(var+eps) for each row of X.""" + row = tl.program_id(0).to(tl.int64) + cols = tl.arange(0, BLOCK_SIZE) + mask = cols < K + + x = tl.load(X_ptr + row * X_row_stride + cols, mask=mask, other=0.0) + mean = tl.sum(x, axis=0) / K + centered = tl.where(mask, x - mean, 0.0) + var = tl.sum(centered * centered, axis=0) / K + rstd = 1.0 / tl.sqrt(var + eps) + + tl.store(Mean_ptr + row * Mean_row_stride, mean) + tl.store(Rstd_ptr + row * Rstd_row_stride, rstd) + + +@triton.jit +def _lnlin_swiglu_fwd_kernel( + X_ptr, # [M, K] + W_ptr, # [K, 2N] — first half (cols [0:N]) feeds the silu input; + # second half (cols [N:2N]) is the gate output + LN_W_ptr, # [K] + LN_B_ptr, # [K] + Lin_ptr, # [M, 2N] — full linear output (saved for backward) + Out_ptr, # [M, N] — silu(x1) * x2 (input to the next Linear) + Mean_ptr, + Rstd_ptr, + M, + N, + K, + stride_xm, + stride_xk, + stride_wk, + stride_wn, + stride_lin_m, + stride_lin_n, + stride_out_m, + stride_out_n, + HAS_LN_BIAS: tl.constexpr, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, +): + """One program produces a [BLOCK_M, BLOCK_N] tile of `Out`. To avoid + re-reading X for the two halves of W12, the program does TWO matmul + accumulators (against W_a and W_b) in the same K-loop, producing both + halves of the linear output for the same M tile. Then SwiGLU is applied + in registers and both the [M, 2N] linear output and [M, N] swiglu output + are written.""" + pid = tl.program_id(axis=0).to(tl.int64) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + offs_m = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + + mean = tl.load(Mean_ptr + offs_m, mask=offs_m < M, other=0.0) + rstd = tl.load(Rstd_ptr + offs_m, mask=offs_m < M, other=0.0) + + x_ptrs = X_ptr + (offs_m[:, None] * stride_xm + offs_k[None, :] * stride_xk) + wa_ptrs = W_ptr + (offs_k[:, None] * stride_wk + offs_n[None, :] * stride_wn) + wb_ptrs = W_ptr + (offs_k[:, None] * stride_wk + (N + offs_n[None, :]) * stride_wn) + + a_acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + b_acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K)): + kk = k * BLOCK_SIZE_K + k_remaining = K - kk + k_mask = offs_k < k_remaining + + x = tl.load( + x_ptrs, + mask=(offs_m[:, None] < M) & (offs_k[None, :] < k_remaining), + other=0.0, + ) + ln_w = tl.load(LN_W_ptr + kk + offs_k, mask=k_mask, other=0.0) + if HAS_LN_BIAS: + ln_b = tl.load(LN_B_ptr + kk + offs_k, mask=k_mask, other=0.0) + x_hat = ((x - mean[:, None]) * rstd[:, None]) * ln_w[None, :] + ln_b[ + None, : + ] + else: + x_hat = ((x - mean[:, None]) * rstd[:, None]) * ln_w[None, :] + + wa = tl.load( + wa_ptrs, + mask=(offs_k[:, None] < k_remaining) & (offs_n[None, :] < N), + other=0.0, + ) + wb = tl.load( + wb_ptrs, + mask=(offs_k[:, None] < k_remaining) & (offs_n[None, :] < N), + other=0.0, + ) + + a_acc = tl.dot(x_hat, wa, a_acc) + b_acc = tl.dot(x_hat, wb, b_acc) + + x_ptrs += BLOCK_SIZE_K * stride_xk + wa_ptrs += BLOCK_SIZE_K * stride_wk + wb_ptrs += BLOCK_SIZE_K * stride_wk + + a_bf = a_acc.to(Lin_ptr.type.element_ty) + b_bf = b_acc.to(Lin_ptr.type.element_ty) + + out_mask = (offs_m[:, None] < M) & (offs_n[None, :] < N) + lin_a_ptrs = ( + Lin_ptr + offs_m[:, None] * stride_lin_m + offs_n[None, :] * stride_lin_n + ) + lin_b_ptrs = ( + Lin_ptr + offs_m[:, None] * stride_lin_m + (N + offs_n[None, :]) * stride_lin_n + ) + tl.store(lin_a_ptrs, a_bf, mask=out_mask) + tl.store(lin_b_ptrs, b_bf, mask=out_mask) + + # SwiGLU = silu(a) * b. SiLU computed in fp32 for numerical accuracy. + sig = tl.sigmoid(a_acc) + silu_a = a_acc * sig + swiglu = silu_a * b_acc + out_ptrs = Out_ptr + offs_m[:, None] * stride_out_m + offs_n[None, :] * stride_out_n + tl.store(out_ptrs, swiglu.to(Out_ptr.type.element_ty), mask=out_mask) + + +# SwiGLU backward: in-place into the saved linear-output buffer (no alloc). +@triton.jit +def _swiglu_bwd_inplace_kernel( + dout_ptr, lin_ptr, M, N, stride_d, stride_l, BLOCK: tl.constexpr +): + row = tl.program_id(0).to(tl.int64) + cols = tl.arange(0, BLOCK) + mask = cols < N + + a_ptr = lin_ptr + row * stride_l + cols + b_ptr = lin_ptr + row * stride_l + N + cols + do_ptr = dout_ptr + row * stride_d + cols + + a = tl.load(a_ptr, mask=mask, other=0.0).to(tl.float32) + b = tl.load(b_ptr, mask=mask, other=0.0).to(tl.float32) + do = tl.load(do_ptr, mask=mask, other=0.0).to(tl.float32) + + sig = tl.sigmoid(a) + silu = a * sig + # d(silu)/da = sig + silu*(1 - sig) = sig*(1 + a*(1 - sig)) + silu_grad = sig * (1.0 + a * (1.0 - sig)) + + da = do * b * silu_grad + db = do * silu + + tl.store(a_ptr, da.to(lin_ptr.type.element_ty), mask=mask) + tl.store(b_ptr, db.to(lin_ptr.type.element_ty), mask=mask) + + +_FWD_CONFIG_K128 = dict( + BLOCK_SIZE_M=128, + BLOCK_SIZE_N=128, + BLOCK_SIZE_K=32, + GROUP_SIZE_M=8, + num_stages=4, + num_warps=8, +) +_FWD_CONFIG_K256 = dict( + BLOCK_SIZE_M=64, + BLOCK_SIZE_N=64, + BLOCK_SIZE_K=64, + GROUP_SIZE_M=8, + num_stages=3, + num_warps=4, +) + + +def _pick_fwd_config(K: int) -> dict: + if K == 128: + return _FWD_CONFIG_K128 + if K == 256: + return _FWD_CONFIG_K256 + # Reasonable default for other K. May be sub-optimal — autotune for new shapes. + return _FWD_CONFIG_K256 + + +def _next_pow2(n: int) -> int: + p = 1 + while p < n: + p <<= 1 + return p + + +def _ln_stats_settings(K: int) -> tuple[int, int]: + BLOCK = _next_pow2(K) + if BLOCK <= 256: + num_warps = 4 + elif BLOCK <= 1024: + num_warps = 8 + else: + num_warps = 16 + return BLOCK, num_warps + + +def _lnlin_swiglu_fwd( + x_2d: torch.Tensor, W12: torch.Tensor, LN_W: torch.Tensor, LN_B: torch.Tensor | None +): + assert x_2d.is_contiguous(), "X must be contiguous" + M, K = x_2d.shape + K2, two_N = W12.shape + assert K2 == K and two_N % 2 == 0, f"W12 shape mismatch: {W12.shape} vs K={K}" + N = two_N // 2 + + out = torch.empty((M, N), dtype=x_2d.dtype, device=x_2d.device) + lin = torch.empty((M, two_N), dtype=x_2d.dtype, device=x_2d.device) + Mean = torch.empty((M,), dtype=x_2d.dtype, device=x_2d.device) + Rstd = torch.empty((M,), dtype=x_2d.dtype, device=x_2d.device) + + block, num_warps = _ln_stats_settings(K) + _ln_stats_kernel[(M,)]( + x_2d, + x_2d.stride(0), + Mean, + Mean.stride(0), + Rstd, + Rstd.stride(0), + K, + 1e-5, + BLOCK_SIZE=block, + num_warps=num_warps, # pyright: ignore[reportCallIssue] + ) + + cfg = _pick_fwd_config(K) + grid = lambda META: ( + triton.cdiv(M, META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]), + ) + _lnlin_swiglu_fwd_kernel[grid]( + x_2d, + W12, + LN_W, + LN_B if LN_B is not None else LN_W, # ptr always non-null + lin, + out, + Mean, + Rstd, + M, + N, + K, + x_2d.stride(0), + x_2d.stride(1), + W12.stride(0), + W12.stride(1), + lin.stride(0), + lin.stride(1), + out.stride(0), + out.stride(1), + HAS_LN_BIAS=(LN_B is not None), + BLOCK_SIZE_M=cfg["BLOCK_SIZE_M"], + BLOCK_SIZE_N=cfg["BLOCK_SIZE_N"], + BLOCK_SIZE_K=cfg["BLOCK_SIZE_K"], + GROUP_SIZE_M=cfg["GROUP_SIZE_M"], + num_stages=cfg["num_stages"], # pyright: ignore[reportCallIssue] + num_warps=cfg["num_warps"], # pyright: ignore[reportCallIssue] + ) + return out, lin, Mean, Rstd + + +def _swiglu_bwd_inplace(dout: torch.Tensor, lin: torch.Tensor) -> torch.Tensor: + """In-place SwiGLU backward: writes da, db into the [M, 2N] `lin` buffer. + + Returns the same tensor (now containing dlin values).""" + M, N = dout.shape + BLOCK = _next_pow2(N) + _swiglu_bwd_inplace_kernel[(M,)]( + dout, + lin, + M, + N, + dout.stride(0), + lin.stride(0), + BLOCK=BLOCK, + num_warps=4, # pyright: ignore[reportCallIssue] + ) + return lin + + +class FusedLNLinearSwiGLUFunction(torch.autograd.Function): + """ + Forward: out = silu(x1) * x2 where (x1, x2) = chunk(LN(X) @ W12, 2) + Backward: standard chain via cuBLAS gemms + ATen native_layer_norm_backward. + """ + + @staticmethod + @torch.amp.custom_fwd(device_type="cuda", cast_inputs=torch.bfloat16) + def forward(ctx, X, W12, LN_W, LN_B): + x_shape = X.shape + x_2d = X.contiguous().view(-1, x_shape[-1]) + out, lin, mean, rstd = _lnlin_swiglu_fwd(x_2d, W12, LN_W, LN_B) + ctx.save_for_backward(x_2d, W12, LN_W, LN_B, mean, rstd, lin) + ctx.x_shape = x_shape + ctx.has_ln_bias = LN_B is not None + return out.view(*x_shape[:-1], out.shape[-1]) + + @staticmethod + @torch.amp.custom_bwd(device_type="cuda") + def backward(ctx, dout): + x_2d, W12, LN_W, LN_B, mean, rstd, lin = ctx.saved_tensors + dout_2d = dout.contiguous().view(-1, dout.shape[-1]) + K = x_2d.shape[1] + + # SwiGLU backward, in-place into the saved `lin` buffer (no new alloc). + dlin = _swiglu_bwd_inplace(dout_2d, lin) + + # LN+Linear backward via cuBLAS + ATen LN bwd. + # x_norm needed for dW12; recomputed via F.layer_norm (cuDNN, ~100 µs at d=256). + x_norm = F.layer_norm(x_2d, (K,), LN_W, LN_B, eps=1e-5) + dW12 = x_norm.transpose(0, 1) @ dlin # [K, 2N] + dx_norm = dlin @ W12.transpose(0, 1) # [M, K] + del x_norm + + # native_layer_norm_backward output_mask = [need_dX, need_dGamma, need_dBeta]. + # We always need dX and dGamma; dBeta only when LN bias was used. + output_mask = [True, True, ctx.has_ln_bias] + dX, dLN_W, dLN_B = torch.ops.aten.native_layer_norm_backward( + dx_norm, x_2d, [K], mean.float(), rstd.float(), LN_W, LN_B, output_mask + ) + # If LN_B was None (no bias), ATen returns dLN_B=None and we just pass it through. + return dX.view(ctx.x_shape), dW12, dLN_W, dLN_B + + +class FusedLNLinearSwiGLU(nn.Module): + """Fused module for `LayerNorm(d) -> Linear(d, 2*hidden) -> silu(x1)*x2`. + + Convention: silu of FIRST half. Output: [..., hidden].""" + + def __init__( + self, + d_model: int, + d_inner: int, + has_ln_bias: bool = True, + device=None, + dtype=None, + ): + super().__init__() + factory = {"device": device, "dtype": dtype} + self.d_model = d_model + self.d_inner = d_inner + self.LN_W = nn.Parameter(torch.empty(d_model, **factory)) + self.LN_B = ( + nn.Parameter(torch.empty(d_model, **factory)) if has_ln_bias else None + ) + self.W12 = nn.Parameter(torch.empty(d_model, 2 * d_inner, **factory)) + self.reset_parameters() + + def reset_parameters(self): + nn.init.ones_(self.LN_W) + if self.LN_B is not None: + nn.init.zeros_(self.LN_B) + bound = 1.0 / math.sqrt(self.d_model) + nn.init.uniform_(self.W12, -bound, bound) + + def forward(self, X: torch.Tensor) -> torch.Tensor: + return FusedLNLinearSwiGLUFunction.apply( # type: ignore[return-value] + X, self.W12, self.LN_W, self.LN_B + ) diff --git a/src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py b/src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py new file mode 100644 index 000000000000..98c1b757c50e --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py @@ -0,0 +1,242 @@ +"""Triton kernel for trimul stage-3 batched einsum in native (D, B, L, L) layout. + + outgoing: ``out[d,b,i,j] = sum_k a[d,b,i,k] * b[d,b,j,k]`` (TRANSPOSE_B=True) + incoming: ``out[d,b,i,j] = sum_k a[d,b,k,i] * b[d,b,k,j]`` (TRANSPOSE_A=True) + +All three tensors share the dense ``(D, B, L, L)`` row-major layout produced +upstream by ``fused_gated_dual_gemm_split`` and consumed downstream by +``layer_norm_transpose(..., layout="dbij->bijd")``. Operating natively in +this layout avoids the contiguous copy that ``torch.einsum``'s autograd +inserts in its backward when saved tensors don't match its expected layout. + +A single kernel covers all 4 transpose patterns (fwd + 4 bwd grad patterns) +via ``TRANSPOSE_A`` / ``TRANSPOSE_B`` constexpr flags. +""" + +import torch +import triton +import triton.language as tl + +# Static config — runtime autotune cold-start is unshippable for inference. +# Triton fwd loses to cuBLAS bgemm; bwd wins by avoiding torch.einsum's +# autograd-internal contiguous copy on (D, B, L, L) saved tensors. +_AUTOTUNE_CONFIGS = [ + triton.Config( + {"TILE_M": 128, "TILE_N": 128, "TILE_K": 64, "GROUP_M": 8}, + num_stages=3, + num_warps=8, + ) +] + + +@triton.autotune(configs=_AUTOTUNE_CONFIGS, key=["L", "TRANSPOSE_A", "TRANSPOSE_B"]) +@triton.jit +def _batched_einsum_kernel( + a_ptr, # (D, B, L, L) row-major + b_ptr, # (D, B, L, L) row-major + out_ptr, # (D, B, L, L) row-major + D, + B, + L, + stride_a_d, # = B*L*L + stride_a_b, # = L*L + stride_b_d, + stride_b_b, + stride_out_d, + stride_out_b, + TILE_M: tl.constexpr, + TILE_N: tl.constexpr, + TILE_K: tl.constexpr, + GROUP_M: tl.constexpr, + TRANSPOSE_A: tl.constexpr, + TRANSPOSE_B: tl.constexpr, +): + # Grid: (num_m * num_n, D * B); (m, n) swizzled for L2 reuse, (d, b) on axis-1. + pid_mn = tl.program_id(axis=0) + pid_db = tl.program_id(axis=1) + + pid_d = pid_db // B + pid_b = pid_db % B + + num_pid_m = tl.cdiv(L, TILE_M) + num_pid_n = tl.cdiv(L, TILE_N) + num_pid_in_group = GROUP_M * num_pid_n + group_id = pid_mn // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + ((pid_mn % num_pid_in_group) % group_size_m) + pid_n = (pid_mn % num_pid_in_group) // group_size_m + + # int64 indexing — (D,B,L,L) at L=1024 D=128 is well over 2**31. + pid_d = tl.cast(pid_d, tl.int64) + pid_b = tl.cast(pid_b, tl.int64) + pid_m_i = tl.cast(pid_m, tl.int64) + pid_n_i = tl.cast(pid_n, tl.int64) + L_i = tl.cast(L, tl.int64) + + a_plane = a_ptr + pid_d * stride_a_d + pid_b * stride_a_b + b_plane = b_ptr + pid_d * stride_b_d + pid_b * stride_b_b + out_plane = out_ptr + pid_d * stride_out_d + pid_b * stride_out_b + + offs_m = pid_m_i * TILE_M + tl.arange(0, TILE_M).to(tl.int64) + offs_n = pid_n_i * TILE_N + tl.arange(0, TILE_N).to(tl.int64) + offs_k = tl.arange(0, TILE_K).to(tl.int64) + + mask_m = offs_m < L_i + mask_n = offs_n < L_i + + if TRANSPOSE_A: + a_ptrs = a_plane + (offs_k[:, None] * L_i + offs_m[None, :]) + a_step = TILE_K * L_i + else: + a_ptrs = a_plane + (offs_m[:, None] * L_i + offs_k[None, :]) + a_step = TILE_K + + if TRANSPOSE_B: + b_ptrs = b_plane + (offs_n[None, :] * L_i + offs_k[:, None]) + b_step = TILE_K + else: + b_ptrs = b_plane + (offs_k[:, None] * L_i + offs_n[None, :]) + b_step = TILE_K * L_i + + acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + n_k_tiles = tl.cdiv(L, TILE_K) + for kk in range(0, n_k_tiles): + k_remaining = L_i - kk * TILE_K + mask_k = offs_k < k_remaining + + if TRANSPOSE_A: + a_mask = mask_k[:, None] & mask_m[None, :] + a_tile = tl.load(a_ptrs, mask=a_mask, other=0.0) + a_tile = tl.trans(a_tile) # tl.dot wants (M, K) + else: + a_mask = mask_m[:, None] & mask_k[None, :] + a_tile = tl.load(a_ptrs, mask=a_mask, other=0.0) + + if TRANSPOSE_B: + b_mask = mask_k[:, None] & mask_n[None, :] + b_tile = tl.load(b_ptrs, mask=b_mask, other=0.0) + else: + b_mask = mask_k[:, None] & mask_n[None, :] + b_tile = tl.load(b_ptrs, mask=b_mask, other=0.0) + + acc = tl.dot(a_tile, b_tile, acc) + + a_ptrs += a_step + b_ptrs += b_step + + out_ptrs = out_plane + (offs_m[:, None] * L_i + offs_n[None, :]) + out_mask = mask_m[:, None] & mask_n[None, :] + tl.store(out_ptrs, acc.to(out_ptr.type.element_ty), mask=out_mask) + + +def _batched_einsum( + a: torch.Tensor, b: torch.Tensor, transpose_a: bool, transpose_b: bool +) -> torch.Tensor: + """Compute ``(A op_a) @ (B op_b)`` per (d, b) plane. + + Parameters + ---------- + a, b : torch.Tensor + Shape (D, B, L, L), bf16, contiguous. + transpose_a : bool + If True, contract over the row dim of A (k = leading), else over col dim. + transpose_b : bool + If True, B is read as (n, k) (i.e. B^T enters the matmul: out += A·B^T), + else as (k, n) (out += A·B). + + Returns + ------- + out : torch.Tensor + Shape (D, B, L, L), bf16. + """ + assert a.shape == b.shape, f"a {a.shape} ≠ b {b.shape}" + assert a.ndim == 4 + assert a.dtype == torch.bfloat16 and b.dtype == torch.bfloat16 + assert ( + a.is_contiguous() and b.is_contiguous() + ), "trimul einsum kernel requires contiguous (D,B,L,L) inputs" + + D, B, L_row, L_col = a.shape + assert L_row == L_col, "L_row must equal L_col for stage-3 einsum" + L = L_row + + out = torch.empty_like(a) + + stride_a_d, stride_a_b = a.stride(0), a.stride(1) + stride_b_d, stride_b_b = b.stride(0), b.stride(1) + stride_out_d, stride_out_b = out.stride(0), out.stride(1) + + def grid(meta): + num_m = triton.cdiv(L, meta["TILE_M"]) + num_n = triton.cdiv(L, meta["TILE_N"]) + return (num_m * num_n, D * B) + + _batched_einsum_kernel[grid]( + a, + b, + out, + D, + B, + L, + stride_a_d, + stride_a_b, + stride_b_d, + stride_b_b, + stride_out_d, + stride_out_b, + TRANSPOSE_A=transpose_a, + TRANSPOSE_B=transpose_b, + ) + return out + + +class TrimulBatchedEinsum(torch.autograd.Function): + """Autograd-aware wrapper for trimul stage-3 batched einsum. + + Forward direction is one of: + "outgoing": out[d,b,i,j] = sum_k a[d,b,i,k] * b[d,b,j,k] (A · B^T) + "incoming": out[d,b,i,j] = sum_k a[d,b,k,i] * b[d,b,k,j] (A^T · B) + """ + + @staticmethod + def forward( # type: ignore[override] + ctx, a: torch.Tensor, b: torch.Tensor, direction: str + ) -> torch.Tensor: + if direction == "outgoing": + out = _batched_einsum(a, b, transpose_a=False, transpose_b=True) + elif direction == "incoming": + out = _batched_einsum(a, b, transpose_a=True, transpose_b=False) + else: + raise ValueError(f"unknown direction {direction!r}") + ctx.save_for_backward(a, b) + ctx.direction = direction + return out + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] + a, b = ctx.saved_tensors + direction = ctx.direction + if not grad_out.is_contiguous(): + grad_out = grad_out.contiguous() + + if direction == "outgoing": + grad_a = _batched_einsum(grad_out, b, transpose_a=False, transpose_b=False) + grad_b = _batched_einsum(grad_out, a, transpose_a=True, transpose_b=False) + else: # incoming + grad_a = _batched_einsum(b, grad_out, transpose_a=False, transpose_b=True) + grad_b = _batched_einsum(a, grad_out, transpose_a=False, transpose_b=False) + return grad_a, grad_b, None + + +def trimul_batched_einsum( + a: torch.Tensor, b: torch.Tensor, direction: str +) -> torch.Tensor: + """Public entry: dispatches to autograd Function if grad is enabled.""" + if torch.is_grad_enabled() and (a.requires_grad or b.requires_grad): + return TrimulBatchedEinsum.apply(a, b, direction) # type: ignore[return-value] + if direction == "outgoing": + return _batched_einsum(a, b, transpose_a=False, transpose_b=True) + elif direction == "incoming": + return _batched_einsum(a, b, transpose_a=True, transpose_b=False) + raise ValueError(f"unknown direction {direction!r}") diff --git a/src/transformers/models/esmfold2/kernels/trimul_with_residual.py b/src/transformers/models/esmfold2/kernels/trimul_with_residual.py new file mode 100644 index 000000000000..45ec395c6169 --- /dev/null +++ b/src/transformers/models/esmfold2/kernels/trimul_with_residual.py @@ -0,0 +1,622 @@ +"""TriMul with output residual + dropout-mask epilogue fused into the final GEMM. + +Inspired by cuequivariance's ``triangle_multiplicative_update`` +(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently +re-implemented in Triton with the output residual and dropout mask folded +into the final gated GEMM so the ``delta = TriMul(pair)`` intermediate is +never written to HBM. + +Replaces the standard pattern + + pair_new = pair + dropout_mask * triangle_multiplicative_update(pair) + +— where the two ops materialize the full ``delta = TriMul(pair)`` tensor +between them — with a single fused path. The final gated GEMM fuses three +operations end-to-end: + + 1. Computes ``delta = sigmoid(x_in @ Wg) * (x_out @ Wp)`` per output tile. + 2. Multiplies by the row-shared dropout mask in-register + (``mask[b, j, d]`` indexed by decoded ``b, j`` from the flat ``m`` index). + 3. Adds the prior pair residual loaded from HBM. + 4. Writes the new pair tensor. + +Saves: one full pair-tensor write (``delta``) + one full pair-tensor read +(``delta`` re-read by FusedDropoutResidual). Roughly ~250 MB per call at +L=768, B=5 in bf16; compounds across loops. + +Forward + backward both implemented for the bf16 path. fp8 IO paths and +``transpose_out`` remain inference-only. The backward kernel recomputes +``gate = sigmoid(x1 @ w1.T)`` and ``val = x2 @ w2.T`` (the two dual GEMMs) +to keep saved-context cost down, and emits per-element +``d_gate_logits``, ``d_val_acc``, ``d_drop_mask_partials``; outer cuBLAS +calls handle the weight/input GEMM reductions. The residual add is +identity in the backward: ``d_residual = d_out``. +""" +# ruff: noqa: E402 + +from __future__ import annotations + +import os +import warnings + +warnings.filterwarnings("ignore", category=FutureWarning) +warnings.filterwarnings("ignore", category=UserWarning) +warnings.filterwarnings("ignore", category=DeprecationWarning) +os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1") +os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1") + +import torch +import triton +import triton.language as tl + +from .fused_dual_gemm import fused_gated_dual_gemm_split +from .fused_ln_residual import fused_ln_transpose, fused_ln_with_residual_link +from .trimul_einsum_triton import trimul_batched_einsum + +# Static config — runtime autotune cold-start is unshippable for inference. +_AUTOTUNE_CONFIGS = [ + triton.Config( + {"TILE_M": 64, "TILE_N": 64, "TILE_K": 64, "GROUP_M": 8}, + num_stages=3, + num_warps=4, + ) +] + + +@triton.autotune(configs=_AUTOTUNE_CONFIGS, key=["M", "N", "K", "HAS_DROP_MASK"]) +@triton.jit +def _gated_gemm_with_residual_kernel( + x1_ptr, # [M, K] — gate input (pre-residual pair) + x2_ptr, # [M, K] — value input (post-LN_out) + w1_ptr, # [N, K] — gate weight + w2_ptr, # [N, K] — value weight + residual_ptr, # [M, N] pair tensor pre-TriMul + drop_mask_ptr, # [B*N_COL, N] row-shared dropout mask + o_ptr, # [M, N] output = residual + drop_mask * sigmoid(x1@w1) * (x2@w2) + M, + N, + K, + STRIDE_B: tl.constexpr, # = N_ROW * N_COL (for decoding b from m) + N_COL: tl.constexpr, # = L_col (for decoding j from m) + TILE_M: tl.constexpr, + TILE_N: tl.constexpr, + TILE_K: tl.constexpr, + GROUP_M: tl.constexpr, + PRECISION: tl.constexpr, + HAS_DROP_MASK: tl.constexpr, + NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] +): + pid_m_raw = tl.program_id(axis=0) + pid_n_raw = tl.program_id(axis=1) + + # GROUP_M swizzle for L2 reuse of x rows across consecutive CTAs. + num_pid_m = tl.cdiv(M, TILE_M) + num_pid_n = tl.cdiv(N, TILE_N) + pid = pid_n_raw * num_pid_m + pid_m_raw + num_pid_in_group = GROUP_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + if NEEDS_INT64: + pid_m = tl.cast(pid_m, tl.int64) + pid_n = tl.cast(pid_n, tl.int64) + M = tl.cast(M, tl.int64) + N = tl.cast(N, tl.int64) + K = tl.cast(K, tl.int64) + + start_m = pid_m * TILE_M + start_n = pid_n * TILE_N + + offs_xm = start_m + tl.arange(0, TILE_M) + offs_wn = start_n + tl.arange(0, TILE_N) + offs_k = tl.arange(0, TILE_K) + + if NEEDS_INT64: + offs_xm = tl.cast(offs_xm, tl.int64) + offs_wn = tl.cast(offs_wn, tl.int64) + offs_k = tl.cast(offs_k, tl.int64) + + x1_ptrs = x1_ptr + (offs_xm[:, None] * K + offs_k[None, :]) + x2_ptrs = x2_ptr + (offs_xm[:, None] * K + offs_k[None, :]) + w_tile_offs = offs_wn[None, :] * K + offs_k[:, None] + + acc_1 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + acc_2 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + mask_m = offs_xm < M + + for _ in range(0, tl.cdiv(K, TILE_K)): + x1 = tl.load(x1_ptrs, mask=mask_m[:, None], other=0.0).to( + w1_ptr.type.element_ty + ) + w1_ptrs = w1_ptr + w_tile_offs + w1 = tl.load(w1_ptrs) + if PRECISION == 0: + acc_1 = tl.dot(x1, w1, acc_1) + elif PRECISION == 2: + acc_1 = tl.dot(x1, w1, acc_1, input_precision="tf32x3") + else: + acc_1 = tl.dot(x1, w1, acc_1, input_precision="ieee") + x1_ptrs += TILE_K + w1_ptr += TILE_K + + for _ in range(0, tl.cdiv(K, TILE_K)): + x2 = tl.load(x2_ptrs, mask=mask_m[:, None], other=0.0).to( + w2_ptr.type.element_ty + ) + w2_ptrs = w2_ptr + w_tile_offs + w2 = tl.load(w2_ptrs) + if PRECISION == 0: + acc_2 = tl.dot(x2, w2, acc_2) + elif PRECISION == 2: + acc_2 = tl.dot(x2, w2, acc_2, input_precision="tf32x3") + else: + acc_2 = tl.dot(x2, w2, acc_2, input_precision="ieee") + x2_ptrs += TILE_K + w2_ptr += TILE_K + + acc_1 = 1.0 / (1.0 + tl.exp(-acc_1)) + delta = acc_1 * acc_2 + + offs_om = pid_m * TILE_M + tl.arange(0, TILE_M) + offs_on = pid_n * TILE_N + tl.arange(0, TILE_N) + if NEEDS_INT64: + offs_om = tl.cast(offs_om, tl.int64) + offs_on = tl.cast(offs_on, tl.int64) + + # Row-shared dropout: mask is [B*N_COL, N]; decode (b, j) from m. + if HAS_DROP_MASK: + b = offs_om // STRIDE_B + j = offs_om % N_COL + mask_row = b * N_COL + j + drop_ptrs = drop_mask_ptr + mask_row[:, None] * N + offs_on[None, :] + drop_tile = tl.load(drop_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + delta = delta * drop_tile + + resid_ptrs = residual_ptr + offs_om[:, None] * N + offs_on[None, :] + resid_tile = tl.load(resid_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + out_val = delta + resid_tile + + o_ptrs = o_ptr + offs_om[:, None] * N + offs_on[None, :] + o_mask = mask_m[:, None] + tl.store(o_ptrs, out_val.to(o_ptr.type.element_ty), mask=o_mask) + + +# Backward recomputes the two fwd GEMMs, emits per-element grads for the +# outer cuBLAS GEMMs (d_x1, d_x2, d_w1, d_w2). d_residual is identity through +# the residual add. d_drop_mask is row-shared → per-pid_n partial + host reduce. + + +# Narrow TILE_N=32 in bwd: register pressure is dominated by the two output +# gradient tensors (vs forward's single output). +_BWD_AUTOTUNE_CONFIGS = [ + triton.Config( + {"TILE_M": 64, "TILE_N": 32, "TILE_K": 64, "GROUP_M": 8}, + num_stages=3, + num_warps=4, + ) +] + + +@triton.autotune(configs=_BWD_AUTOTUNE_CONFIGS, key=["M", "N", "K", "HAS_DROP_MASK"]) +@triton.jit +def _gated_gemm_with_residual_backward_kernel( + grad_out_ptr, # [M, N] bf16 — incoming grad + x1_ptr, # [M, K] bf16 — saved gate input + x2_ptr, # [M, K] bf16 — saved value input + w1_ptr, # [N, K] bf16 + w2_ptr, # [N, K] bf16 + drop_mask_ptr, # [B*N_COL, N] bf16 — row-shared (unused if HAS_DROP_MASK=0) + grad_gate_logits_ptr, # [M, N] bf16 — out + grad_val_acc_ptr, # [M, N] bf16 — out + grad_drop_mask_buf_ptr, # [B*N_COL, N] fp32 — drop_mask grad accumulator (atomic_add) + M, + N, + K, + STRIDE_B: tl.constexpr, + N_COL: tl.constexpr, + TILE_M: tl.constexpr, + TILE_N: tl.constexpr, + TILE_K: tl.constexpr, + GROUP_M: tl.constexpr, + HAS_DROP_MASK: tl.constexpr, + NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] +): + pid_m_raw = tl.program_id(axis=0) + pid_n_raw = tl.program_id(axis=1) + + num_pid_m = tl.cdiv(M, TILE_M) + num_pid_n = tl.cdiv(N, TILE_N) + pid = pid_n_raw * num_pid_m + pid_m_raw + num_pid_in_group = GROUP_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + if NEEDS_INT64: + pid_m = tl.cast(pid_m, tl.int64) + pid_n = tl.cast(pid_n, tl.int64) + M = tl.cast(M, tl.int64) + N = tl.cast(N, tl.int64) + K = tl.cast(K, tl.int64) + + start_m = pid_m * TILE_M + start_n = pid_n * TILE_N + + offs_xm = start_m + tl.arange(0, TILE_M) + offs_wn = start_n + tl.arange(0, TILE_N) + offs_k = tl.arange(0, TILE_K) + if NEEDS_INT64: + offs_xm = tl.cast(offs_xm, tl.int64) + offs_wn = tl.cast(offs_wn, tl.int64) + offs_k = tl.cast(offs_k, tl.int64) + + x1_ptrs = x1_ptr + (offs_xm[:, None] * K + offs_k[None, :]) + x2_ptrs = x2_ptr + (offs_xm[:, None] * K + offs_k[None, :]) + w_tile_offs = offs_wn[None, :] * K + offs_k[:, None] + + acc_1 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + acc_2 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) + mask_m = offs_xm < M + + w1p = w1_ptr + for _ in range(0, tl.cdiv(K, TILE_K)): + x1 = tl.load(x1_ptrs, mask=mask_m[:, None], other=0.0).to( + w1_ptr.type.element_ty + ) + w1 = tl.load(w1p + w_tile_offs) + acc_1 = tl.dot(x1, w1, acc_1) + x1_ptrs += TILE_K + w1p += TILE_K + + w2p = w2_ptr + for _ in range(0, tl.cdiv(K, TILE_K)): + x2 = tl.load(x2_ptrs, mask=mask_m[:, None], other=0.0).to( + w2_ptr.type.element_ty + ) + w2 = tl.load(w2p + w_tile_offs) + acc_2 = tl.dot(x2, w2, acc_2) + x2_ptrs += TILE_K + w2p += TILE_K + + g = tl.sigmoid(acc_1) + delta = g * acc_2 # pre-mask delta (forward post-mask = delta * drop_mask) + + offs_om = pid_m * TILE_M + tl.arange(0, TILE_M) + offs_on = pid_n * TILE_N + tl.arange(0, TILE_N) + if NEEDS_INT64: + offs_om = tl.cast(offs_om, tl.int64) + offs_on = tl.cast(offs_on, tl.int64) + + grad_o_ptrs = grad_out_ptr + offs_om[:, None] * N + offs_on[None, :] + grad_o = tl.load(grad_o_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + + if HAS_DROP_MASK: + # Multiple m's collide on the same mask_row, so use fp32 atomic_add to a + # (B*N_COL, N) accumulator (handles intra- and inter-tile collisions). + b = offs_om // STRIDE_B + j = offs_om % N_COL + mask_row = b * N_COL + j + drop_ptrs = drop_mask_ptr + mask_row[:, None] * N + offs_on[None, :] + drop_tile = tl.load(drop_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) + + d_drop_per_elem = grad_o * delta + acc_ptrs = grad_drop_mask_buf_ptr + mask_row[:, None] * N + offs_on[None, :] + tl.atomic_add(acc_ptrs, d_drop_per_elem, mask=mask_m[:, None]) + + grad_o = grad_o * drop_tile + + d_val_acc = (grad_o * g).to(grad_val_acc_ptr.type.element_ty) + d_val_acc_ptrs = grad_val_acc_ptr + offs_om[:, None] * N + offs_on[None, :] + tl.store(d_val_acc_ptrs, d_val_acc, mask=mask_m[:, None]) + + d_gate_logits = (grad_o * acc_2 * g * (1.0 - g)).to( + grad_gate_logits_ptr.type.element_ty + ) + d_gate_logits_ptrs = grad_gate_logits_ptr + offs_om[:, None] * N + offs_on[None, :] + tl.store(d_gate_logits_ptrs, d_gate_logits, mask=mask_m[:, None]) + + +def _gated_gemm_with_residual_bwd( + grad_out: torch.Tensor, # [M, N] bf16 + x1: torch.Tensor, # [M, K] bf16 + x2: torch.Tensor, # [M, K] bf16 + w1: torch.Tensor, + w2: torch.Tensor, + drop_mask: torch.Tensor | None, + n_row: int, + n_col: int, +) -> tuple[ + torch.Tensor, + torch.Tensor, + torch.Tensor, + torch.Tensor, + torch.Tensor, + torch.Tensor | None, +]: + """Returns (d_x1, d_x2, d_w1, d_w2, d_residual, d_drop_mask).""" + M, K = x1.shape + N = w1.shape[0] + assert x2.shape == x1.shape + assert ( + w1.dtype == w2.dtype == torch.bfloat16 + ), f"weights must be bf16; got {w1.dtype}/{w2.dtype}" + assert x1.dtype == torch.bfloat16 and x2.dtype == torch.bfloat16 + + # Don't call .contiguous() unconditionally (avoids a full-tensor clone). + if not grad_out.is_contiguous(): + grad_out = grad_out.contiguous() + grad_out_2d = grad_out.view(M, N) + x1_c = x1 if x1.is_contiguous() else x1.contiguous() + x2_c = x2 if x2.is_contiguous() else x2.contiguous() + w1_c = w1 if w1.is_contiguous() else w1.contiguous() + w2_c = w2 if w2.is_contiguous() else w2.contiguous() + + grad_gate_logits = torch.empty((M, N), device=x1.device, dtype=torch.bfloat16) + grad_val_acc = torch.empty((M, N), device=x1.device, dtype=torch.bfloat16) + + # fp32 accumulator for row-shared mask_row collisions across m. + if drop_mask is not None: + drop_mask = drop_mask.contiguous().view(-1, N) + n_mask_rows = drop_mask.shape[0] + grad_drop_buf = torch.zeros( + (n_mask_rows, N), device=x1.device, dtype=torch.float32 + ) + else: + grad_drop_buf = torch.empty((1,), device=x1.device, dtype=torch.float32) + + _dummy_mask = torch.zeros((), device=x1.device, dtype=torch.bfloat16) + + NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * N >= 2**31 - 1) + + def grid(meta): + return (triton.cdiv(M, meta["TILE_M"]), triton.cdiv(N, meta["TILE_N"])) + + _gated_gemm_with_residual_backward_kernel[grid]( + grad_out_2d, + x1_c, + x2_c, + w1_c, + w2_c, + drop_mask if drop_mask is not None else _dummy_mask, + grad_gate_logits, + grad_val_acc, + grad_drop_buf, + M, + N, + K, + STRIDE_B=n_row * n_col, + N_COL=n_col, + HAS_DROP_MASK=drop_mask is not None, + NEEDS_INT64=NEEDS_INT64, + ) + + d_w1 = grad_gate_logits.t() @ x1_c # [N, K] + d_w2 = grad_val_acc.t() @ x2_c # [N, K] + d_x1 = grad_gate_logits @ w1_c # [M, K] + d_x2 = grad_val_acc @ w2_c # [M, K] + + d_residual = grad_out_2d + + if drop_mask is not None: + d_drop_mask = grad_drop_buf.to(drop_mask.dtype) + else: + d_drop_mask = None + + return d_x1, d_x2, d_w1, d_w2, d_residual, d_drop_mask + + +class GatedGEMMWithResidual(torch.autograd.Function): + """Autograd wrapper for ``_gated_gemm_with_residual`` (bf16 path).""" + + @staticmethod + def forward( + ctx, + x1: torch.Tensor, + x2: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + residual: torch.Tensor, + drop_mask: torch.Tensor | None, + n_row: int, + n_col: int, + precision: int, + ) -> torch.Tensor: + out = _gated_gemm_with_residual_fwd( + x1, x2, w1, w2, residual, drop_mask, n_row, n_col, precision=precision + ) + if drop_mask is None: + ctx.save_for_backward(x1, x2, w1, w2) + ctx.has_drop_mask = False + else: + ctx.save_for_backward(x1, x2, w1, w2, drop_mask) + ctx.has_drop_mask = True + ctx.n_row = n_row + ctx.n_col = n_col + return out + + @staticmethod + def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] + if ctx.has_drop_mask: + x1, x2, w1, w2, drop_mask = ctx.saved_tensors + else: + x1, x2, w1, w2 = ctx.saved_tensors + drop_mask = None + d_x1, d_x2, d_w1, d_w2, d_res, d_drop = _gated_gemm_with_residual_bwd( + grad_out.contiguous(), x1, x2, w1, w2, drop_mask, ctx.n_row, ctx.n_col + ) + return d_x1, d_x2, d_w1, d_w2, d_res, d_drop, None, None, None + + +def _gated_gemm_with_residual_fwd( + x1: torch.Tensor, + x2: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + residual: torch.Tensor, + drop_mask: torch.Tensor | None, + n_row: int, + n_col: int, + precision: int = 0, +) -> torch.Tensor: + """Run the residual+dropout-aware final GEMM. + + Shapes: x1, x2: (M, K). w1, w2: (N, K). residual: (M, N). drop_mask: (B*n_col, N) or None. + Output: (M, N). + """ + M, K = x1.shape + N = w1.shape[0] + x1 = x1.contiguous() + x2 = x2.contiguous() + w1 = w1.contiguous() + w2 = w2.contiguous() + residual = residual.contiguous().view(M, N) + if drop_mask is not None: + drop_mask = drop_mask.contiguous().view(-1, N) + out = residual.new_empty((M, N)) + + NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * N >= 2**31 - 1) + + def grid(meta): + return (triton.cdiv(M, meta["TILE_M"]), triton.cdiv(N, meta["TILE_N"])) + + _gated_gemm_with_residual_kernel[grid]( + x1, + x2, + w1, + w2, + residual, + drop_mask if drop_mask is not None else residual, # dummy pass-through + out, + M, + N, + K, + STRIDE_B=n_row * n_col, + N_COL=n_col, + PRECISION=precision, + HAS_DROP_MASK=drop_mask is not None, + NEEDS_INT64=NEEDS_INT64, + ) + return out + + +def _gated_gemm_with_residual( + x1: torch.Tensor, + x2: torch.Tensor, + w1: torch.Tensor, + w2: torch.Tensor, + residual: torch.Tensor, + drop_mask: torch.Tensor | None, + n_row: int, + n_col: int, + precision: int = 0, +) -> torch.Tensor: + """Public wrapper: dispatches to autograd Function if grad is enabled.""" + if torch.is_grad_enabled() and ( + x1.requires_grad + or x2.requires_grad + or w1.requires_grad + or w2.requires_grad + or residual.requires_grad + ): + return GatedGEMMWithResidual.apply( # type: ignore[return-value] + x1, x2, w1, w2, residual, drop_mask, n_row, n_col, precision + ) + return _gated_gemm_with_residual_fwd( + x1, x2, w1, w2, residual, drop_mask, n_row, n_col, precision=precision + ) + + +@torch._dynamo.disable +def triangle_multiplicative_update_with_residual( + pair: torch.Tensor, + direction: str, + residual: torch.Tensor, + drop_mask: torch.Tensor | None, + *, + norm_in_weight: torch.Tensor, + norm_in_bias: torch.Tensor, + p_in_weight: torch.Tensor, + g_in_weight: torch.Tensor, + norm_out_weight: torch.Tensor, + norm_out_bias: torch.Tensor, + p_out_weight: torch.Tensor, + g_out_weight: torch.Tensor, + mask: torch.Tensor | None = None, + eps: float = 1e-5, + precision: int = 0, +) -> torch.Tensor: + """Fused TriMul-and-residual: pair_new = residual + drop_mask * TriMul(pair). + + The intermediate ``delta = TriMul(pair)`` is never materialized in HBM — + the dropout-mask multiply and residual-add happen in-kernel at the final + GEMM. Saves one pair-tensor write + read vs the + ``TriMul → FusedDropoutResidual`` baseline. + + Shapes: + pair, residual: (B, L, L, c_z) + drop_mask: (B, 1, L, c_z) or (B*L, c_z) — row-shared + mask: (B, L, L) or None + weights: (N, K) bf16 — see arg list for each stage. + """ + assert pair.shape == residual.shape + B, L_row, L_col, c_z = pair.shape + + # Stage 1: input LayerNorm. When ``residual is pair`` (common case for trimul), + # use the fused LN-fwd + LN-bwd-with-residual variant so the bwd pass adds + # ``grad_residual`` into ``grad_x`` in one kernel (saves one pair-tensor + # HBM round-trip). + if residual is pair: + x, residual_alias = fused_ln_with_residual_link( + pair, norm_in_weight, norm_in_bias, pair, eps=eps, layout="bijd->bijd" + ) + else: + x = fused_ln_transpose( + pair, norm_in_weight, norm_in_bias, eps=eps, layout="bijd->bijd" + ) + residual_alias = residual + x_in = x + + # Stage 2: gated dual GEMM → ab (transposed for the einsum's dbij layout). + # Train path runs transpose_out=False (no bwd for in-kernel transpose), + # then a view/permute reaches the dbij layout downstream. + a, b_t = fused_gated_dual_gemm_split(x, g_in_weight, p_in_weight, mask=mask) + + # Stage 3: triangular einsum. + # Training: native (D, B, L, L) Triton kernel — its bwd reads the layout + # natively, eliminating the contiguous copy torch.einsum's autograd does. + # Inference: torch.einsum (cuBLAS bgemm) is faster fwd-only. + if torch.is_grad_enabled(): + x = trimul_batched_einsum(a, b_t, direction) + elif direction == "outgoing": + x = torch.einsum("dbik,dbjk->dbij", a, b_t) + else: + x = torch.einsum("dbki,dbkj->dbij", a, b_t) + + # Stage 4: output LayerNorm (back to bijd). + x_out = fused_ln_transpose( + x, norm_out_weight, norm_out_bias, eps=eps, layout="dbij->bijd" + ) + + # Stage 5: fused output gated GEMM + dropout mask + residual add. + x_in_2d = x_in.reshape(-1, c_z) + x_out_2d = x_out.reshape(-1, c_z) + # Stage-5 residual MUST be the LN1 alias when LN1-residual-fusion is on, + # otherwise the grad routing through the fused LN bwd won't fire. + residual_2d = residual_alias.reshape(-1, c_z) + out_2d = _gated_gemm_with_residual( + x_in_2d, + x_out_2d, + g_out_weight, + p_out_weight, + residual_2d, + drop_mask, + n_row=L_row, + n_col=L_col, + precision=precision, + ) + return out_2d.view(B, L_row, L_col, c_z) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py new file mode 100644 index 000000000000..79024feb0285 --- /dev/null +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -0,0 +1,1163 @@ +"""PyTorch ESMFold2 model — the standard released architecture. + +Quickstart:: + + from transformers import ESMFold2Model + + model = ESMFold2Model.from_pretrained("biohub/ESMFold2").cuda().eval() + open("ubq.pdb", "w").write(model.infer_protein_as_pdb("MQIFVKTLTGKT...")) + +For multi-chain / ligand / MSA inputs see ``ESMFold2InputBuilder`` in the +companion ``esm`` package. +""" + +import math +from contextlib import contextmanager +from typing import cast + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor + +try: + import transformer_engine.pytorch as te # type: ignore[import] + from transformer_engine.common.recipe import ( # type: ignore[import] + DelayedScaling, + Format, + ) + + TE_AVAILABLE = True +except ImportError: + te = None # type: ignore[assignment] + DelayedScaling = None # type: ignore[assignment] + Format = None # type: ignore[assignment] + TE_AVAILABLE = False + +from ...modeling_utils import PreTrainedModel # type: ignore[import] +from .configuration_esmfold2 import ESMFold2Config +from .modeling_esmfold2_common import ( + CHAR_VOCAB_SIZE, + MAX_ATOMIC_NUMBER, + NUM_RES_TYPES, + DiffusionStructureHead, + FoldingTrunk, + InputsEmbedder, + LanguageModelShim, + MSAPairWeightedAveraging, + OuterProductMean, + ResIdxAsymIdSymIdEntityIdEncoding, + RowAttentionPooling, + SwiGLUMLP, + TriangleMultiplicativeUpdate, + _categorical_mean, + _compute_intra_token_idx, + compute_lm_hidden_states, + gather_rep_atom_coords, + gather_token_to_atom, +) + +_EPS = 1e-6 +_NONPOLYMER_ID = 4 + +# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory +# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; +# chunk=64 leaves headroom for the largest foldbench targets). Override via +# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for +# short L but OOM-prone past ~600). +_DEFAULT_CHUNK_SIZE = 64 + + +class PairTransition(nn.Module): + """LayerNorm + SwiGLU feed-forward residual block on the pair representation.""" + + def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: + super().__init__() + self.norm = nn.LayerNorm(d_model) + self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def forward(self, x: Tensor) -> Tensor: + if self._chunk_size is None or x.shape[1] <= self._chunk_size: + return self.ffn(self.norm(x)) + out: list[Tensor] = [] + for s in range(0, x.shape[1], self._chunk_size): + e = min(s + self._chunk_size, x.shape[1]) + sl = x[:, s:e] + out.append(self.ffn(self.norm(sl))) + return torch.cat(out, dim=1) + + +class ConfidenceHead(nn.Module): + """Predicts pLDDT, PAE, PDE, resolved-atom probability and distogram bins.""" + + boundaries: Tensor + + def __init__(self, config: "ESMFold2Config") -> None: + super().__init__() + ch = config.confidence_head + d_single = config.d_single + d_pair = config.d_pair + d_inputs = config.inputs.d_inputs + + boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1) + self.register_buffer("boundaries", boundaries) + self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) + + self.s_norm = nn.LayerNorm(d_single) + self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) + self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) + self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) + self.s_inputs_norm = nn.LayerNorm(d_inputs) + self.z_norm = nn.LayerNorm(d_pair) + + self.row_attention_pooling = RowAttentionPooling( + d_pair=d_pair, d_single=d_single + ) + + pf = ch.folding_trunk + self.folding_trunk = FoldingTrunk( + n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 + ) + + # Heads. + self.plddt_ln = nn.LayerNorm(d_single) + max_atoms_per_token = 23 + self.plddt_weight = nn.Parameter( + torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins) + ) + + self.pae_ln = nn.LayerNorm(d_pair) + self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) + + self.pde_ln = nn.LayerNorm(d_pair) + self.pde_head = nn.Linear(d_pair, ch.num_pde_bins, bias=False) + + self.resolved_ln = nn.LayerNorm(d_single) + # 2 = resolved logits ([unresolved, resolved]). + self.resolved_weight = nn.Parameter( + torch.zeros(max_atoms_per_token, d_single, 2) + ) + + def set_kernel_backend(self, backend: str | None) -> None: + self.folding_trunk.set_kernel_backend(backend) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.folding_trunk.set_chunk_size(chunk_size) + + @staticmethod + def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor: + return ( + x + if num_diffusion_samples == 1 + else x.repeat_interleave(num_diffusion_samples, 0) + ) + + @staticmethod + def _flatten_sample_axis(x: Tensor) -> Tensor: + if x.ndim == 4: + b, mult, n, c = x.shape + return x.reshape(b * mult, n, c) + return x + + def forward( + self, + s_inputs: Tensor, + z: Tensor, + x_pred: Tensor, + distogram_atom_idx: Tensor, + token_attention_mask: Tensor, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + asym_id: Tensor, + mol_type: Tensor, + num_diffusion_samples: int = 1, + relative_position_encoding: Tensor | None = None, + token_bonds_encoding: Tensor | None = None, + ) -> dict[str, Tensor]: + s_inputs_normed = self.s_inputs_norm(s_inputs) + + z_base = self.z_norm(z) + if relative_position_encoding is not None: + z_base = z_base + relative_position_encoding + if token_bonds_encoding is not None: + z_base = z_base + token_bonds_encoding + z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2) + z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1) + z_base = z_base + self.s_to_z_prod_out( + self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] + * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] + ) + + pair = self._repeat_batch(z_base, num_diffusion_samples) + x_pred_flat = self._flatten_sample_axis(x_pred) + atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples) + atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples) + rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long() + mask = self._repeat_batch(token_attention_mask, num_diffusion_samples) + Bm = pair.shape[0] + + rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) + rep_distances = torch.cdist( + rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist" + ) + distogram_bins = ( + (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() + ) + pair = pair + self.dist_bin_pairwise_embed(distogram_bins) + + pair_mask = mask[:, :, None].float() * mask[:, None, :].float() + + # FoldingTrunk handles the bf16 cast internally during inference so + # each block's fused trimul engages. In-place residual avoids an + # extra fp32 pair allocation. + with torch.amp.autocast("cuda", enabled=pair.is_cuda, dtype=torch.bfloat16): + pair_delta = self.folding_trunk(pair, pair_attention_mask=pair_mask) + pair.add_(pair_delta.float()) + del pair_delta + single = self.row_attention_pooling(pair, mask) + + atom_mask_f = atom_mask_m.float() + s_at_atoms = gather_token_to_atom(single, atom_to_token_m) + s_at_atoms_ln = self.plddt_ln(s_at_atoms) + + intra_idx = _compute_intra_token_idx(atom_to_token_m) + intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) + w_plddt = self.plddt_weight[intra_idx] + plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms_ln, w_plddt) + plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0) + + L = single.shape[1] + plddt_sum = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) + atom_count = torch.zeros( + Bm, L, device=single.device, dtype=plddt_per_atom.dtype + ) + atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype) + plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t) + atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) + plddt = plddt_sum / atom_count.clamp(min=1e-6) + + complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / ( + atom_mask_f.sum(dim=-1) + _EPS + ) + + expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) + expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) + is_ligand = (expanded_type == _NONPOLYMER_ID).float() + inter_chain = ( + expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) + ).float() + near_contact = (rep_distances < 8).float() + interface_per_token = ( + near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1) + ).amax(dim=-1) + iplddt_weight = torch.where( + is_ligand.bool(), + torch.full_like(interface_per_token, 2.0), + interface_per_token, + ) + iplddt_weight_atoms = gather_token_to_atom( + iplddt_weight.unsqueeze(-1), atom_to_token_m + ).squeeze(-1) + atom_iplddt_w = atom_mask_f * iplddt_weight_atoms + complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / ( + atom_iplddt_w.sum(dim=-1) + _EPS + ) + + plddt_ca = plddt_per_atom.gather(1, rep_idx_m) + + # PAE + pae_logits = self.pae_head(self.pae_ln(pair)) + pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() + + # PDE + pde_logits = self.pde_head(self.pde_ln(pair)) + pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() + + # Resolved (per-atom binary). + s_at_atoms_res = self.resolved_ln(s_at_atoms) + w_res = self.resolved_weight[intra_idx] + resolved_logits = torch.einsum("...c,...cb->...b", s_at_atoms_res, w_res) + + # pTM / ipTM from pae_logits. + n_bins = pae_logits.shape[-1] + bin_width = 32.0 / n_bins + bin_centers = torch.arange( + 0.5 * bin_width, 32.0, bin_width, device=pae_logits.device + ) + mask_f = mask.float() + N_res = mask_f.sum(dim=-1, keepdim=True) + d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 + tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2) + pae_probs = F.softmax(pae_logits, dim=-1) + tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1) + + pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) + ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / ( + pair_mask_2d.sum(dim=-1) + _EPS + ) + ptm = ptm_per_row.max(dim=-1).values + + inter_chain_mask = ( + expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) + ).float() * pair_mask_2d + iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / ( + inter_chain_mask.sum(dim=-1) + _EPS + ) + iptm = iptm_per_row.max(dim=-1).values + + max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 + n_chains = max_chain_id + 1 + pair_chains_iptm = torch.zeros( + Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype + ) + for c1 in range(n_chains): + chain_c1 = (expanded_asym == c1).float() * mask_f + if chain_c1.sum() == 0: + continue + for c2 in range(n_chains): + chain_c2 = (expanded_asym == c2).float() * mask_f + pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) + denom = pair_m.sum(dim=(-1, -2)) + _EPS + pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum( + dim=(-1, -2) + ) / denom + + return { + "plddt_logits": plddt_logits, + "plddt": plddt.detach(), + "plddt_per_atom": plddt_per_atom.detach(), + "plddt_ca": plddt_ca.detach(), + "complex_plddt": complex_plddt.detach(), + "complex_iplddt": complex_iplddt.detach(), + "pae_logits": pae_logits, + "pae": pae, + "pde_logits": pde_logits, + "pde": pde, + "resolved_logits": resolved_logits, + "ptm": ptm.detach(), + "iptm": iptm.detach(), + "pair_chains_iptm": pair_chains_iptm.detach(), + } + + +def _inverse_softplus(value: float) -> float: + return value + math.log(-math.expm1(-value)) + + +def _convert_te_modules_to_fp8_inplace(module: nn.Module) -> None: + """Re-init each TE module via quantized_model_init so weights live as fp8. + + Must be called inside torch.no_grad(); covers nn.Linear, te.Linear, + te.LayerNormLinear, te.LayerNormMLP — the last two hold 99% of ESMC weight. + """ + if not TE_AVAILABLE: + raise RuntimeError("transformer_engine is not available; cannot use fp8.") + from transformer_engine.pytorch import quantized_model_init # type: ignore[import] + + def _walk(mod: nn.Module) -> None: + for name, child in list(mod.named_children()): + replaced = False + if isinstance(child, nn.Linear): + in_f, out_f = child.in_features, child.out_features + has_bias = child.bias is not None + device = child.weight.device + dtype = child.weight.dtype + w = child.weight.data + b = child.bias.data if has_bias else None + setattr(mod, name, nn.Identity()) + del child + torch.cuda.empty_cache() + with quantized_model_init(enabled=True): + new_mod = te.Linear( # type: ignore[union-attr] + in_f, out_f, bias=has_bias, params_dtype=dtype + ).to(device) + new_mod.weight.quantize_(w) # type: ignore[attr-defined,operator] + if has_bias: + assert b is not None + new_mod.bias.data.copy_(b) # type: ignore[union-attr] + del w, b + replaced = True + elif isinstance(child, te.Linear): # type: ignore[union-attr] + # te.Linear with bf16 weight → re-init inside quantized_model_init for fp8. + in_f, out_f = child.in_features, child.out_features + has_bias = child.bias is not None + device = child.weight.device + dtype = ( + child.weight.dtype + if not hasattr(child.weight, "_data") + else torch.bfloat16 + ) + state = {k: v.detach().clone() for k, v in child.state_dict().items()} + setattr(mod, name, nn.Identity()) + del child + torch.cuda.empty_cache() + with quantized_model_init(enabled=True): + new_mod = te.Linear( # type: ignore[union-attr] + in_f, + out_f, + bias=has_bias, + params_dtype=dtype, # type: ignore[arg-type] + ).to(device) # type: ignore[arg-type] + new_mod.load_state_dict(state, strict=False) + replaced = True + elif ( + hasattr(te, "LayerNormLinear") and isinstance(child, te.LayerNormLinear) # type: ignore[union-attr] + ): + state = {k: v.detach().clone() for k, v in child.state_dict().items()} + hidden_size = child.in_features + out_features = child.out_features + has_bias = child.use_bias + device = next(child.parameters()).device + setattr(mod, name, nn.Identity()) + del child + torch.cuda.empty_cache() + with quantized_model_init(enabled=True): + new_mod = te.LayerNormLinear( # type: ignore[union-attr] + hidden_size, + out_features, + bias=has_bias, + params_dtype=torch.bfloat16, + ).to(device) + new_mod.load_state_dict(state, strict=False) + replaced = True + elif ( + hasattr(te, "LayerNormMLP") and isinstance(child, te.LayerNormMLP) # type: ignore[union-attr] + ): + state = {k: v.detach().clone() for k, v in child.state_dict().items()} + fc1_weight: Tensor = child.fc1_weight # type: ignore[attr-defined] + hidden_size = int(fc1_weight.shape[1]) + # fc1 packed as (2*ffn_hidden_size, hidden_size) for swiglu. + ffn_hidden_size = int(fc1_weight.shape[0]) // 2 + has_bias = ( + getattr(child, "fc1_bias", None) is not None + and child.fc1_bias is not None # type: ignore[attr-defined] + ) + device = fc1_weight.device + setattr(mod, name, nn.Identity()) + del child + torch.cuda.empty_cache() + with quantized_model_init(enabled=True): + new_mod = te.LayerNormMLP( # type: ignore[union-attr] + hidden_size=hidden_size, + ffn_hidden_size=ffn_hidden_size, + bias=has_bias, + activation="swiglu", + params_dtype=torch.bfloat16, + ).to(device) # type: ignore[arg-type] + new_mod.load_state_dict(state, strict=False) + replaced = True + + if replaced: + # Freeze via .eval()+.requires_grad_(False); per-param ops would unwrap Float8Tensor. + new_mod.eval().requires_grad_(False) + setattr(mod, name, new_mod) + torch.cuda.empty_cache() + else: + _walk(child) + + _walk(module) + torch.cuda.empty_cache() + + +@contextmanager +def _lm_precision_context(fp8: bool): + """bf16 autocast (+ optional TE fp8 autocast) around the LM forward. + + te.autocast keeps te.Linear outputs bf16 instead of the fp32 default + (~425 MB at L=1024 in the hidden-state cache). + """ + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + if fp8 and TE_AVAILABLE: + fp8_recipe = DelayedScaling( # type: ignore[misc] + fp8_format=Format.HYBRID, # type: ignore[union-attr] + amax_history_len=1, + amax_compute_algo="most_recent", + ) + with te.autocast(enabled=True, recipe=fp8_recipe): # type: ignore[union-attr] + yield + else: + yield + + +class ESMFold2Model(PreTrainedModel): + """ESMFold2 — all-atom structure prediction with an ESMC PLM backbone. + + This is the standard released ESMFold2 architecture (uses a linear- + recurrent trunk, internally referred to as "parcae"). + + Forward kwargs that callers commonly override: + + * ``num_loops`` (default ``config.num_loops``): trunk refinement + loops. + * ``num_diffusion_samples`` (default ``config.num_diffusion_samples``): + parallel structure samples; the confidence head re-runs once per + sample, so memory scales linearly. Pass ``1`` for cheap inference. + * ``num_sampling_steps`` (default ``config.structure_head.inference_num_steps``): + diffusion ODE solver steps. Lower for speed, higher for quality. + + Memory / perf knobs: + + * ``model.set_chunk_size(int|None)``: caps L² ops (triangle / OPM / + pair transition) at this token-axis chunk. Default 64 — fits + L≈2k on an 80 GB GPU. Pass ``None`` for faster inference at L<600. + * ``model.set_kernel_backend(None | "fused" | "cuequivariance")``: + select kernel backend (None = reference path). + """ + + config_class = ESMFold2Config + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__(config) + d_inputs = config.inputs.d_inputs + d_pair = config.d_pair + + self.inputs_embedder = InputsEmbedder(config) + self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) + self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False) + self.rel_pos = ResIdxAsymIdSymIdEntityIdEncoding( + n_relative_residx_bins=config.n_relative_residx_bins, + n_relative_chain_bins=config.n_relative_chain_bins, + d_pair=d_pair, + ) + self.token_bonds = nn.Linear(1, d_pair, bias=False) + self.language_model = LanguageModelShim( + d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers + ) + self._esmc: nn.Module | None = None + self._esmc_fp8: bool = False # set by load_esmc(fp8=True) + + pf = config.folding_trunk + self.folding_trunk = FoldingTrunk( + n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 + ) + if config.lm_encoder.enabled: + self.lm_encoder: FoldingTrunk | None = FoldingTrunk( + n_layers=config.lm_encoder.n_layers, d_pair=d_pair, expansion_ratio=4 + ) + else: + self.lm_encoder = None + + self.parcae_input_norm = nn.LayerNorm(d_pair) + self.parcae_log_a = nn.Parameter(torch.zeros(d_pair)) + parcae_decay_init = math.sqrt(1.0 / 5.0) + parcae_delta_init = -math.log(parcae_decay_init) + self.parcae_log_delta = nn.Parameter( + torch.full( + (d_pair,), _inverse_softplus(parcae_delta_init), dtype=torch.float32 + ) + ) + self.parcae_b_cont = nn.Parameter(torch.eye(d_pair)) + self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) + nn.init.eye_(self.parcae_readout.weight) + self.parcae_coda = FoldingTrunk( + n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4 + ) + + # Heads -------------------------------------------------------------- + self.structure_head = DiffusionStructureHead(config) + self.distogram_head = nn.Linear( + d_pair, config.structure_head.distogram_bins, bias=True + ) + self.confidence_head = ConfidenceHead(config) + + msa_cfg = config.msa_encoder + self.msa_encoder = None + if msa_cfg.enabled: + self.msa_encoder = MSAEncoder( + d_msa=msa_cfg.d_msa, + d_pair=d_pair, + d_inputs=d_inputs, + d_hidden=msa_cfg.d_hidden, + n_layers=msa_cfg.n_layers, + n_heads_msa=msa_cfg.n_heads_msa, + msa_head_width=msa_cfg.msa_head_width, + ) + + self.post_init() + + def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: + """Load the ESMC LM. + + ``precision``: ``"bf16"`` (default), ``"fp32"``, or ``"fp8"``. + ``"fp8"`` requires H100 + TransformerEngine ≥ 2.x and quantizes + every TE module's weights to fp8 storage. + """ + from ..esmc.modeling_esmc import ESMCModel + + dtype_map = { + "bf16": torch.bfloat16, + "fp32": torch.float32, + "fp8": torch.bfloat16, # underlying weights stay bf16, TE re-quantizes to fp8 + } + if precision not in dtype_map: + raise ValueError( + f"precision must be one of {list(dtype_map)}, got {precision!r}" + ) + dtype = dtype_map[precision] + + esmc = ( + ESMCModel.from_pretrained(esmc_model_path) + .to(device=self.device, dtype=dtype) + .eval() + ) + for p in esmc.parameters(): + p.requires_grad_(False) + + if precision == "fp8": + if not TE_AVAILABLE: + raise RuntimeError( + "transformer_engine is not available; cannot use fp8." + ) + with torch.no_grad(): + _convert_te_modules_to_fp8_inplace(esmc) + self._esmc_fp8 = True + else: + self._esmc_fp8 = False + + self._esmc = esmc + + @classmethod + def from_pretrained( + cls, pretrained_model_name_or_path, *args, load_esmc: bool = True, **kwargs + ): + if cls is ESMFold2Model and "config" not in kwargs: + config = ESMFold2Config.from_pretrained( + pretrained_model_name_or_path, **kwargs + ) + if config.type == "experimental": + from .modeling_esmfold2_experimental import ESMFold2ExperimentalModel + + return ESMFold2ExperimentalModel.from_pretrained( + pretrained_model_name_or_path, + *args, + config=config, + load_esmc=load_esmc, + **kwargs, + ) + kwargs["config"] = config + # Pop the precision knob before forwarding to the HF loader. + esmc_precision = kwargs.pop("esmc_precision", "bf16") + model = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs) + if load_esmc: + model.load_esmc(model.config.esmc_id, precision=esmc_precision) + return model + + def set_kernel_backend(self, backend: str | None) -> None: + """Select kernel backend. + + Args: + backend: ``None`` (reference path), ``"fused"`` (vendored Triton + kernels), or ``"cuequivariance"`` (cuequivariance kernels + where applicable; vanilla python fallback otherwise). + """ + self.folding_trunk.set_kernel_backend(backend) + if self.lm_encoder is not None: + self.lm_encoder.set_kernel_backend(backend) + self.parcae_coda.set_kernel_backend(backend) + self.confidence_head.set_kernel_backend(backend) + self.structure_head.set_kernel_backend(backend) + + def apply_torch_compile( + self, mode: str = "fixed_seqlen", dynamic: bool | None = None + ) -> None: + """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once. + + Does NOT stack with our Triton kernels — call ``set_kernel_backend(None)`` + before compiling. + """ + import torch._dynamo + + torch._dynamo.config.cache_size_limit = 512 # type: ignore[attr-defined] + torch._dynamo.config.accumulated_cache_size_limit = 512 # type: ignore[attr-defined] + # capture_scalar_outputs avoids graph breaks at .item() in atom-attention path. + torch._dynamo.config.capture_scalar_outputs = True # type: ignore[attr-defined] + + if dynamic is None: + dynamic = mode == "dynamic_seqlen" + kwargs: dict = {"dynamic": dynamic} + + from .modeling_esmfold2_common import ( + DiffusionModule, + DiffusionTransformer, + PairUpdateBlock, + ) + + compile_targets = ( + PairUpdateBlock, + DiffusionTransformer, + DiffusionModule, + MSAEncoderBlock, + ) + + def _maybe_compile(module: nn.Module) -> None: + if isinstance(module, compile_targets): + module.forward = torch.compile(module.forward, **kwargs) # type: ignore[assignment] + + self.apply(_maybe_compile) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.folding_trunk.set_chunk_size(chunk_size) + if self.lm_encoder is not None: + self.lm_encoder.set_chunk_size(chunk_size) + self.parcae_coda.set_chunk_size(chunk_size) + self.confidence_head.set_chunk_size(chunk_size) + if self.msa_encoder is not None: + self.msa_encoder.set_chunk_size(chunk_size) + + def _compute_lm_hidden_states( + self, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + tok_mask: Tensor, + ) -> Tensor: + assert self._esmc is not None + # fp8 TE kernels require prod(shape[:-1]) % 8 == 0. + pad_to = 8 if self._esmc_fp8 else None + with _lm_precision_context(self._esmc_fp8): + return compute_lm_hidden_states( + self._esmc, + input_ids, + asym_id, + residue_index, + mol_type, + tok_mask, + pad_to_multiple=pad_to, + ) + + def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: + delta = F.softplus(self.parcae_log_delta) + a = torch.exp(-delta * torch.exp(self.parcae_log_a)) + b = delta[:, None] * self.parcae_b_cont + return a, b + + def _init_pair_state(self, ref: Tensor) -> Tensor: + std = math.sqrt(2.0 / (5.0 * ref.shape[-1])) + state = torch.empty_like(ref, dtype=torch.float32) + nn.init.trunc_normal_(state, mean=0.0, std=std, a=-3 * std, b=3 * std) + return state.to(dtype=ref.dtype) + + def _run_one_loop( + self, + z: Tensor, + z_init: Tensor, + lm_z: Tensor | None, + _msa_kwargs: dict | None, + pair_mask: Tensor, + a: Tensor, + b_mat: Tensor, + total_steps: int, + ) -> Tensor: + # Helper method (not inline) so per-iter locals free on return — + # otherwise leaks ~2 GB L²×c_z into distogram/sample scope. + # training=True forces dropout under eval(), matching the per-loop + # dropout strategy used at train time. + lm_cfg = self.config.lm_encoder + _per_loop_lm_dropout = ( + lm_z is not None + and getattr(lm_cfg, "per_loop_lm_dropout", False) + and getattr(lm_cfg, "lm_dropout", 0.0) > 0.0 + ) + _lm_dropout_p = getattr(lm_cfg, "lm_dropout", 0.0) + + for _ in range(total_steps): + if _per_loop_lm_dropout: + assert lm_z is not None # narrowed by _per_loop_lm_dropout + lm_z_i: Tensor | None = F.dropout(lm_z, p=_lm_dropout_p, training=True) + else: + lm_z_i = lm_z + + refined_lm_z: Tensor | None = None + if lm_z_i is not None and self.lm_encoder is not None: + refined_lm_z = self.lm_encoder( + lm_z_i.to(z_init.dtype), pair_attention_mask=pair_mask + ) + + z_inject_pair = z_init + if lm_z_i is not None and self.lm_encoder is None: + z_inject_pair = z_inject_pair + lm_z_i.to(z_inject_pair.dtype) + + if self.msa_encoder is not None and _msa_kwargs is not None: + msa_pair = self.msa_encoder(x_pair=z_inject_pair, **_msa_kwargs).to( + z_inject_pair.dtype + ) + z_inject_pair = ( + msa_pair + if self.config.msa_encoder_overwrite + else (z_inject_pair + msa_pair) + ) + + if refined_lm_z is not None: + z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) + + injected_pair = self.parcae_input_norm(z_inject_pair) + z = a * z + F.linear(injected_pair.to(z.dtype), b_mat) + z = self.folding_trunk(z, pair_attention_mask=pair_mask) + + return z + + @torch.inference_mode() + def forward( + self, + token_index: Tensor, + residue_index: Tensor, + asym_id: Tensor, + sym_id: Tensor, + entity_id: Tensor, + mol_type: Tensor, + res_type: Tensor, + token_bonds: Tensor, + token_attention_mask: Tensor, + ref_pos: Tensor, + ref_element: Tensor, + ref_charge: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + atom_attention_mask: Tensor, + atom_to_token: Tensor, + distogram_atom_idx: Tensor, + deletion_mean: Tensor | None = None, + msa: Tensor | None = None, + has_deletion: Tensor | None = None, + deletion_value: Tensor | None = None, + msa_attention_mask: Tensor | None = None, + input_ids: Tensor | None = None, + lm_hidden_states: Tensor | None = None, + num_loops: int | None = None, + num_diffusion_samples: int | None = None, + num_sampling_steps: int | None = None, + **kwargs, + ) -> dict[str, Tensor]: + tok_mask = token_attention_mask + atm_mask = atom_attention_mask + disto_idx = distogram_atom_idx + + n_loops: int = num_loops if num_loops is not None else self.config.num_loops + n_samples: int = ( + num_diffusion_samples + if num_diffusion_samples is not None + else self.config.num_diffusion_samples + ) + total_steps = max(1, n_loops + 1) + + if res_type.dim() == 2: + res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float() + res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() + else: + res_type_oh = res_type.float() + + if msa is not None: + msa_oh_profile = F.one_hot(msa.long(), num_classes=NUM_RES_TYPES).float() + if msa_attention_mask is not None: + mask_f = msa_attention_mask.float().unsqueeze(-1) + msa_oh_profile = msa_oh_profile * mask_f + valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) + profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) + else: + profile = msa_oh_profile.mean(dim=1) + else: + profile = res_type_oh + + if deletion_mean is None: + deletion_mean = torch.zeros( + res_type.shape[0], res_type.shape[1], device=res_type.device + ) + + ref_element_oh = F.one_hot( + ref_element.long(), num_classes=MAX_ATOMIC_NUMBER + ).float() + ref_atom_name_chars_oh = F.one_hot( + ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE + ).float() + # Bias-free downstream Linears require zeroed padding. + atm_mask_f = atm_mask.float() + ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) + ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze( + -1 + ).unsqueeze(-1) + atom_to_token = atom_to_token * atm_mask.long() + + use_amp = ref_pos.device.type == "cuda" + with torch.amp.autocast("cuda", enabled=use_amp, dtype=torch.bfloat16): + x_inputs = self.inputs_embedder( + aatype=res_type_oh, + profile=profile.float(), + deletion_mean=deletion_mean.float(), + ref_pos=ref_pos, + atom_attention_mask=atm_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element_oh, + ref_atom_name_chars=ref_atom_name_chars_oh, + atom_to_token=atom_to_token, + ) + + z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2( + x_inputs + ).unsqueeze(1) + + relative_position_encoding = self.rel_pos( + residue_index=residue_index, + asym_id=asym_id, + sym_id=sym_id, + entity_id=entity_id, + token_index=token_index, + ) + token_bonds_encoding = self.token_bonds(token_bonds.float()) + z_init = z_init + relative_position_encoding + token_bonds_encoding + + if ( + lm_hidden_states is None + and input_ids is not None + and self._esmc is not None + ): + lm_hidden_states = self._compute_lm_hidden_states( + input_ids, asym_id, residue_index, mol_type, tok_mask + ) + lm_z: Tensor | None = None + if lm_hidden_states is not None: + lm_z = self.language_model(lm_hidden_states.detach()) + del lm_hidden_states + + pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() + + z = self._init_pair_state(z_init) + + a, b = self._discretized_dynamics() + a = a.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) + b_mat = b.to(device=z.device, dtype=z.dtype) + + _msa_kwargs: dict | None = None + if self.msa_encoder is not None and msa is not None: + B_msa, M, L_msa = msa.shape + msa_oh = F.one_hot( + msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES + ).float() + msa_attn = ( + msa_attention_mask.permute(0, 2, 1).float() + if msa_attention_mask is not None + else tok_mask[:, :, None].expand(-1, -1, M).float() + ) + # Bias-free MSAEncoder.embed requires zeroed padding. + msa_oh = msa_oh * msa_attn.unsqueeze(-1) + hd = ( + has_deletion.permute(0, 2, 1).float() + if has_deletion is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + dv = ( + deletion_value.permute(0, 2, 1).float() + if deletion_value is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + _msa_kwargs = dict( + x_inputs=x_inputs, + msa_oh=msa_oh, + has_deletion=hd, + deletion_value=dv, + msa_attention_mask=msa_attn, + ) + + # Method call (not inline loop) frees per-iter L²×c_z locals. + z = self._run_one_loop( + z=z, + z_init=z_init, + lm_z=lm_z, + _msa_kwargs=_msa_kwargs, + pair_mask=pair_mask, + a=a, + b_mat=b_mat, + total_steps=total_steps, + ) + del z_init, lm_z, _msa_kwargs, a, b_mat + + z = self.parcae_readout(z) + z = self.parcae_coda(z, pair_attention_mask=pair_mask) + + z = z.float() + distogram_logits = self.distogram_head(z + z.transpose(-2, -3)) + + structure_output = self.structure_head.sample( + z_trunk=z, + s_inputs=x_inputs, + s_trunk=None, + relative_position_encoding=relative_position_encoding, + ref_pos=ref_pos, + ref_charge=ref_charge, + ref_mask=atm_mask, + ref_element=ref_element_oh, + ref_atom_name_chars=ref_atom_name_chars_oh, + ref_space_uid=ref_space_uid, + tok_idx=atom_to_token, + asym_id=asym_id, + residue_index=residue_index, + entity_id=entity_id, + token_index=token_index, + sym_id=sym_id, + token_attention_mask=tok_mask, + num_diffusion_samples=n_samples, + num_sampling_steps=num_sampling_steps, + return_atom_repr=False, + denoising_early_exit_rmsd=None, + ) + + sample_coords = structure_output["sample_atom_coords"] + assert sample_coords is not None + output: dict[str, Tensor] = {"distogram_logits": distogram_logits} + output["sample_atom_coords"] = sample_coords + + confidence_output = self.confidence_head( + s_inputs=x_inputs.detach(), + z=z.detach().float(), + x_pred=sample_coords.detach(), + distogram_atom_idx=disto_idx, + token_attention_mask=tok_mask, + atom_to_token=atom_to_token, + atom_attention_mask=atm_mask, + asym_id=asym_id, + mol_type=mol_type, + num_diffusion_samples=n_samples, + relative_position_encoding=relative_position_encoding.detach(), + token_bonds_encoding=token_bonds_encoding.detach(), + ) + output.update(confidence_output) + output["atom_pad_mask"] = ( + atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask + ) + output["residue_index"] = residue_index + output["entity_id"] = entity_id + return output + + @torch.no_grad() + def infer_protein(self, seq: str, **forward_kwargs) -> dict: + from .protein_utils import OUTPUT_TO_PDB_FEATURE_KEYS, prepare_protein_features + + features = prepare_protein_features(seq) + features = {k: v.to(self.device) for k, v in features.items()} + output = self(**features, **forward_kwargs) + for k in OUTPUT_TO_PDB_FEATURE_KEYS: + output[k] = features[k] + return output + + def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str: + return self.output_to_pdb(self.infer_protein(seq, **forward_kwargs)) + + @staticmethod + def output_to_pdb(output: dict) -> str: + from .protein_utils import output_to_pdb as _output_to_pdb + + return _output_to_pdb(output) + + +class MSAEncoderBlock(nn.Module): + """One MSA encoder block: OPM into pair, MSA pair-weighted averaging, triangle update.""" + + def __init__( + self, + d_msa: int, + d_pair: int, + d_hidden: int, + n_heads_msa: int, + msa_head_width: int, + is_final_block: bool = False, + ) -> None: + super().__init__() + self.is_final_block = is_final_block + self.outer_product_mean = OuterProductMean(d_msa, d_hidden, d_pair) + if not is_final_block: + self.msa_pair_weighted_averaging = MSAPairWeightedAveraging( + d_msa, d_pair, n_heads_msa, msa_head_width + ) + self.msa_transition = PairTransition(d_msa, expansion_ratio=4) + self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = PairTransition(d_pair, expansion_ratio=4) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.outer_product_mean.set_chunk_size(chunk_size) + self.tri_mul_out.set_chunk_size(chunk_size) + self.tri_mul_in.set_chunk_size(chunk_size) + if not self.is_final_block: + self.msa_transition.set_chunk_size(chunk_size) + self.pair_transition.set_chunk_size(chunk_size) + + def forward( + self, + m: Tensor, + pair: Tensor, + msa_attention_mask: Tensor, + pair_attention_mask: Tensor, + ) -> tuple[Tensor, Tensor]: + pair = pair + self.outer_product_mean(m, msa_attention_mask) + if not self.is_final_block: + m = m + self.msa_pair_weighted_averaging(m, pair, pair_attention_mask) + m = m + self.msa_transition(m) + pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) + pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) + pair = pair + self.pair_transition(pair) + return m, pair + + +class MSAEncoder(nn.Module): + """Stack of [`MSAEncoderBlock`] layers that conditions the pair on an MSA.""" + + def __init__( + self, + d_msa: int, + d_pair: int, + d_inputs: int, + d_hidden: int = 32, + n_layers: int = 4, + n_heads_msa: int = 8, + msa_head_width: int = 16, + ) -> None: + super().__init__() + self.embed = nn.Linear(35, d_msa, bias=False) + self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) + self.blocks = nn.ModuleList( + [ + MSAEncoderBlock( + d_msa=d_msa, + d_pair=d_pair, + d_hidden=d_hidden, + n_heads_msa=n_heads_msa, + msa_head_width=msa_head_width, + is_final_block=(i == n_layers - 1), + ) + for i in range(n_layers) + ] + ) + + def set_chunk_size(self, chunk_size: int | None) -> None: + for block in self.blocks: + cast(MSAEncoderBlock, block).set_chunk_size(chunk_size) + + def forward( + self, + x_pair: Tensor, + x_inputs: Tensor, + msa_oh: Tensor, + has_deletion: Tensor, + deletion_value: Tensor, + msa_attention_mask: Tensor, + ) -> Tensor: + # All inputs are pre-transposed to [B, L, M, ...] before calling. + m_feat = torch.cat( + [msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1 + ) + m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2) + tok_mask = msa_attention_mask[:, :, 0].bool() + pair_attention_mask = tok_mask.unsqueeze(2) & tok_mask.unsqueeze(1) + for block in self.blocks: + m, x_pair = block(m, x_pair, msa_attention_mask, pair_attention_mask) + return x_pair diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py new file mode 100644 index 000000000000..a7789647dbae --- /dev/null +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -0,0 +1,2758 @@ +# coding=utf-8 +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +"""Shared building blocks for ESMFold2 HuggingFace model variants.""" + +from __future__ import annotations + +import random +from contextlib import contextmanager +from functools import partial +from typing import cast + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor +from torch.utils.checkpoint import checkpoint + +try: + from flash_attn import ( # type: ignore[import] + flash_attn_func, + flash_attn_varlen_func, + ) + from flash_attn.bert_padding import ( # type: ignore[import] + index_first_axis, + pad_input, + ) + + FLASH_ATTN_AVAILABLE = True +except ImportError: + flash_attn_func = None # type: ignore[assignment] + flash_attn_varlen_func = None # type: ignore[assignment] + index_first_axis = None # type: ignore[assignment] + pad_input = None # type: ignore[assignment] + FLASH_ATTN_AVAILABLE = False + +try: + from cuequivariance_torch import ( # type: ignore[import] + attention_pair_bias as _cue_attn_pair_bias, + ) + from cuequivariance_torch.primitives.triangle import ( # type: ignore[import] + triangle_multiplicative_update as _cue_tri_mul, + ) + + CUE_AVAILABLE = True +except ImportError: + _cue_attn_pair_bias = None # type: ignore[assignment] + _cue_tri_mul = None # type: ignore[assignment] + CUE_AVAILABLE = False + +# Vendored inference-only Triton kernels. +try: + from .kernels import ( + FusedDropoutResidual as _FusedDropoutResidual, # type: ignore[import] + ) + from .kernels import ( + FusedLNLinearSwiGLU as _FusedLNLinearSwiGLU, # type: ignore[import] + ) + from .kernels import fused_pair_bias as _fused_pair_bias # type: ignore[import] + from .kernels import ( # type: ignore[import] + triangle_multiplicative_update_with_residual as _fused_trimul_with_residual, + ) + + TRITON_KERNELS_AVAILABLE = True +except ImportError: + _fused_pair_bias = None # type: ignore[assignment] + _fused_trimul_with_residual = None # type: ignore[assignment] + _FusedLNLinearSwiGLU = None # type: ignore[assignment] + _FusedDropoutResidual = None # type: ignore[assignment] + TRITON_KERNELS_AVAILABLE = False + +from .configuration_esmfold2 import ESMFold2Config + +BACKEND_FUSED = "fused" +BACKEND_CUEQ = "cuequivariance" +_VALID_BACKENDS = (None, BACKEND_FUSED, BACKEND_CUEQ) + + +def _fused_active(module: nn.Module, tensor: Tensor) -> bool: + """Common preconditions for the vendored fused Triton inference kernels.""" + return ( + TRITON_KERNELS_AVAILABLE + and getattr(module, "_kernel_backend", None) == BACKEND_FUSED + and not torch.is_grad_enabled() + and tensor.is_cuda + ) + + +def _cueq_active(module: nn.Module) -> bool: + return CUE_AVAILABLE and getattr(module, "_kernel_backend", None) == BACKEND_CUEQ + + +class DropoutResidual(nn.Module): + """``residual + dropout(delta)`` with row/col-shared dropout. + + Same signature on both paths. ``use_fused_kernels=True`` + ``batch_dim=1`` + routes through ``FusedDropoutResidual`` (single-pass over pair tensor, + in-place residual add). Falls back to unfused otherwise. + """ + + def __init__( + self, r: float, batch_dim: int, use_fused_kernels: bool = False + ) -> None: + super().__init__() + assert batch_dim in (1, 2), f"batch_dim must be 1 or 2, got {batch_dim}" + self._use_fused_kernels = ( + use_fused_kernels and batch_dim == 1 and _FusedDropoutResidual is not None + ) + self._batch_dim = batch_dim + self._r = r + if self._use_fused_kernels: + assert _FusedDropoutResidual is not None + self._impl: nn.Module = _FusedDropoutResidual(r) + else: + self._impl = nn.Dropout(r) + + def forward(self, residual: Tensor, delta: Tensor) -> Tensor: + if self._use_fused_kernels: + return self._impl(residual, delta) + # Unfused: row/col-shared dropout via [1, ...] mask broadcast. + if self._r == 0.0 or not self.training: + return residual + delta + shape = list(delta.shape) + shape[self._batch_dim] = 1 + mask = self._impl(delta.new_ones(shape)) + return residual + delta * mask + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +CHAR_VOCAB_SIZE: int = 64 +MAX_CHARS: int = 4 +XYZ_DIMS: int = 3 +MAX_ATOMIC_NUMBER: int = 128 + +# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 +ATOM_FEATURE_DIM: int = ( + XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS +) + + +NUM_RES_TYPES: int = 33 + +_EPS = 1e-5 + +# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory +# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; +# chunk=64 leaves headroom for the largest foldbench targets). Override via +# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for +# short L but OOM-prone past ~600). +_DEFAULT_CHUNK_SIZE = 64 + + +# =========================================================================== +# Atom-token utilities +# =========================================================================== + + +def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: + """Broadcast per-token features to per-atom features using gather. + + Args: + token_features: [B, L, d] + atom_to_token_idx: [B, A] int64 + + Returns: + [B, A, d] + """ + idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) + return torch.gather(token_features, 1, idx) + + +def scatter_atom_to_token( + atom_features: Tensor, + atom_to_token_idx: Tensor, + n_tokens: int, + atom_mask: Tensor | None = None, +) -> Tensor: + """Aggregate per-atom features to per-token features (mean). + + Args: + atom_features: [B, A, d] + atom_to_token_idx: [B, A] int64 + n_tokens: L + atom_mask: [B, A] bool + + Returns: + [B, L, d] + """ + B, A, d = atom_features.shape + n_out = n_tokens + idx = atom_to_token_idx + if atom_mask is not None: + idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) + n_out = n_tokens + 1 + idx_expanded = idx.unsqueeze(-1).expand(B, A, d) + out = torch.zeros( + B, n_out, d, device=atom_features.device, dtype=atom_features.dtype + ) + out.scatter_reduce_( + 1, idx_expanded, atom_features, reduce="mean", include_self=False + ) + return out[:, :n_tokens, :] + + +def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: + """Gather representative atom coordinates for each token. + + Args: + coords: [B, A, 3] + rep_atom_idx: [B, L] int64 + + Returns: + [B, L, 3] + """ + idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) + return torch.gather(coords, 1, idx) + + +def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: + """Compute local atom index within each token (vectorised). + + Atoms belonging to the same token are contiguous, so this computes a + running count that resets at each token boundary. + + Args: + atom_to_token: [B, A] flat index mapping each atom to its token. + + Returns: + [B, A] tensor with values in [0, max_atoms_per_token - 1]. + """ + same_as_prev = F.pad( + atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False + ) + ones = torch.ones_like(atom_to_token) + cumsum = torch.cumsum(ones, dim=-1) + group_start = cumsum.masked_fill(same_as_prev, 0) + group_start = torch.cummax(group_start, dim=-1).values + return cumsum - group_start + + +def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: + """Expected value of a categorical distribution over evenly-spaced bins. + + Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. + + Args: + logits: [..., n_bins] + start: left boundary + end: right boundary + + Returns: + [...] expected value + """ + n_bins = logits.shape[-1] + edges = torch.linspace( + start, end, n_bins + 1, device=logits.device, dtype=torch.float32 + ) + v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] + return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) + + +# =========================================================================== +# TransitionLayer (used in DiffusionConditioning) +# =========================================================================== + + +class TransitionLayer(nn.Module): + """SwiGLU transition: norm -> a_proj, b_proj -> silu(a)*b -> out_proj.""" + + def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: + super().__init__() + hidden = n * d_model + self.norm = nn.LayerNorm(d_model, eps=eps) + self.a_proj = nn.Linear(d_model, hidden, bias=False) + self.b_proj = nn.Linear(d_model, hidden, bias=False) + self.out_proj = nn.Linear(hidden, d_model, bias=False) + + def forward(self, x: Tensor) -> Tensor: + x = self.norm(x) + a = self.a_proj(x) + b = self.b_proj(x) + return self.out_proj(F.silu(a) * b) + + +# =========================================================================== +# AdaptiveLayerNorm (used in DiffusionTransformer) +# =========================================================================== + + +class AdaptiveLayerNorm(nn.Module): + """Adaptive layer normalization (adaLN-Zero).""" + + def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: + super().__init__() + self.d_model = d_model + self.d_cond = d_cond + self.eps = eps + self.s_scale = nn.Parameter(torch.ones(d_cond)) + self.s_gate = nn.Linear(d_cond, d_model, bias=True) + self.s_shift = nn.Linear(d_cond, d_model, bias=False) + + def forward(self, a: Tensor, s: Tensor) -> Tensor: + a_norm = F.layer_norm(a, (self.d_model,), None, None, self.eps) + s_norm = F.layer_norm(s, (self.d_cond,), self.s_scale, None, self.eps) + return torch.sigmoid(self.s_gate(s_norm)) * a_norm + self.s_shift(s_norm) + + +# =========================================================================== +# FourierEmbedding +# =========================================================================== + + +class FourierEmbedding(nn.Module): + """Fourier embedding: cos(2*pi*(t*w + b)).""" + + w: Tensor + b: Tensor + + def __init__(self, c: int) -> None: + super().__init__() + self.c = c + self.register_buffer("w", torch.randn(c)) + self.register_buffer("b", torch.randn(c)) + + def forward(self, t_hat: Tensor) -> Tensor: + t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) + return torch.cos( + 2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :]) + ) + + +# =========================================================================== +# SwiGLU / SwiGLUMLP +# =========================================================================== + + +def _compute_swiglu_hidden_size(d_model: int, expansion_ratio: int) -> int: + return expansion_ratio * d_model + + +class SwiGLU(nn.Module): + """SwiGLU with packed w12 and output w3.""" + + def __init__( + self, + in_features: int, + hidden_features: int, + out_features: int | None = None, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) + self.w3 = nn.Linear(hidden_features, out_features, bias=bias) + self.hidden_features = hidden_features + + def forward(self, x: Tensor) -> Tensor: + x12 = self.w12(x) + x1, x2 = x12.split(self.hidden_features, dim=-1) + hidden = F.silu(x1) * x2 + return self.w3(hidden) + + +class SwiGLUMLP(SwiGLU): + """SwiGLU MLP with packed weights, no bias.""" + + def __init__( + self, d_model: int, expansion_ratio: int = 4, bias: bool = False + ) -> None: + hidden = _compute_swiglu_hidden_size(d_model, expansion_ratio) + super().__init__( + in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias + ) + + +# =========================================================================== +# SWA Atom Attention components +# =========================================================================== + + +def _rotate_half(x: Tensor) -> Tensor: + x1, x2 = x.chunk(2, dim=-1) + return torch.cat((-x2, x1), dim=-1) + + +def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: + """Apply RoPE with batch-dependent cos/sin. + + Args: + x: [B, L, H, D] + cos: [B, L, D/2] + sin: [B, L, D/2] + """ + ro_dim = cos.shape[-1] * 2 + cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) + sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) + return torch.cat( + [x[..., :ro_dim] * cos + _rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], + dim=-1, + ) + + +@torch.compiler.disable +def build_3d_rope( + ref_pos: Tensor, + ref_space_uid: Tensor, + head_dim: int, + n_spatial_per_axis: int = 4, + n_uid_pairs: int = 2, + spatial_base_freq: float = 10000.0, + uid_base_freq: float = 10.0, +) -> tuple[Tensor, Tensor]: + """Build cos/sin for 3D RoPE + UID RoPE.""" + device = ref_pos.device + B, N = ref_pos.shape[:2] + half_dim = head_dim // 2 + n_spatial_total = 3 * n_spatial_per_axis + + spatial_inv_freq = 1.0 / ( + spatial_base_freq + ** ( + torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) + / n_spatial_per_axis + ) + ) + uid_inv_freq = 1.0 / ( + uid_base_freq + ** ( + torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) + / n_uid_pairs + ) + ) + + pos_f32 = ref_pos.float() + spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) + spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) + + uid_f32 = ref_space_uid.float() + uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) + + n_active = n_spatial_total + n_uid_pairs + freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) + + if n_active < half_dim: + padding = torch.zeros( + B, N, half_dim - n_active, device=device, dtype=torch.float32 + ) + freqs = torch.cat([freqs, padding], dim=-1) + + cos = freqs.cos().to(torch.bfloat16) + sin = freqs.sin().to(torch.bfloat16) + return cos, sin + + +def qk_norm(x: Tensor) -> Tensor: + return F.rms_norm(x, (x.size(-1),)).to(x.dtype) + + +# =========================================================================== +# SwiGLUFFN (atom transformer blocks) +# =========================================================================== + + +class SwiGLUFFN(nn.Module): + """SwiGLU FFN with rounded hidden size for hardware alignment.""" + + def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: + super().__init__() + hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 + self.w_up = nn.Linear(d_model, 2 * hidden_size, bias=False) + self.w_down = nn.Linear(hidden_size, d_model, bias=False) + + def forward(self, x: Tensor) -> Tensor: + x = x.to(self.w_up.weight.dtype) + x1, x2 = self.w_up(x).chunk(2, dim=-1) + return self.w_down(F.silu(x1) * x2) + + +# =========================================================================== +# SWA3DRoPEAttention +# =========================================================================== + + +class SWA3DRoPEAttention(nn.Module): + """Sliding window attention with 3D RoPE. Has Wqkv, gate_proj, out_proj.""" + + def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: + super().__init__() + self.n_heads = n_heads + self.head_dim = d_model // n_heads + self.scale = self.head_dim**-0.5 + self.half_window = half_window + + self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) + self.out_proj = nn.Linear(d_model, d_model, bias=False) + self.gate_proj = nn.Linear(d_model, d_model, bias=False) + + def forward(self, x: Tensor, attention_params: tuple) -> Tensor: + B, N = x.shape[:2] + cos, sin = attention_params[0], attention_params[1] + + x_input = x + qkv = self.Wqkv(x) + qkv = qkv.view(B, N, 3, self.n_heads, self.head_dim).permute(2, 0, 1, 3, 4) + q, k, v = qkv.unbind(0) + q, k = qk_norm(q), qk_norm(k) + + q = apply_rotary_emb_3d(q, cos, sin) + k = apply_rotary_emb_3d(k, cos, sin) + + input_dtype = q.dtype + if q.dtype not in (torch.float16, torch.bfloat16): + q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() + + if len(attention_params) > 2 and FLASH_ATTN_AVAILABLE: + indices, cu_seqlens, max_seqlen = ( + attention_params[2], + attention_params[3], + attention_params[4], + ) + q_unpad = index_first_axis( # type: ignore[misc] + q.reshape(-1, self.n_heads, self.head_dim), indices + ) + k_unpad = index_first_axis( # type: ignore[misc] + k.reshape(-1, self.n_heads, self.head_dim), indices + ) + v_unpad = index_first_axis( # type: ignore[misc] + v.reshape(-1, self.n_heads, self.head_dim), indices + ) + out_unpad = flash_attn_varlen_func( # type: ignore[misc] + q_unpad, + k_unpad, + v_unpad, + cu_seqlens, + cu_seqlens, + max_seqlen, + max_seqlen, + softmax_scale=self.scale, + window_size=(self.half_window, self.half_window), + ) + out = pad_input(out_unpad, indices, B, N) # type: ignore[misc] + elif FLASH_ATTN_AVAILABLE: + out = flash_attn_func( # type: ignore[misc] + q, + k, + v, + softmax_scale=self.scale, + window_size=(self.half_window, self.half_window), + ) + else: + if len(attention_params) > 2: + valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) + valid[attention_params[2]] = True + valid = valid.view(B, N) + else: + valid = torch.ones(B, N, dtype=torch.bool, device=q.device) + rank = torch.cumsum(valid, dim=1) - 1 + within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window + allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) + allowed |= torch.eye(N, dtype=torch.bool, device=q.device) + out = F.scaled_dot_product_attention( + q.transpose(1, 2), + k.transpose(1, 2), + v.transpose(1, 2), + attn_mask=allowed.unsqueeze(1), + scale=self.scale, + ).transpose(1, 2) + out = out * valid.unsqueeze(-1).unsqueeze(-1) + + out = out.to(input_dtype).reshape(B, N, -1) # type: ignore[union-attr] + out = out * torch.sigmoid(self.gate_proj(x_input)) + return self.out_proj(out) + + +# =========================================================================== +# SWAAtomBlock, SWAAtomTransformer +# =========================================================================== + + +def _rms_adaln_raw(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: + return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift + + +def _gated_residual_raw(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: + return x + gate * y + + +class SWAAtomBlock(nn.Module): + """adaLN-Zero + SWA attention + SwiGLU FFN. + + Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight + """ + + def __init__( + self, + d_atom: int, + n_heads: int, + half_window: int = 64, + expansion_ratio: int = 2, + use_compile_fusions: bool = False, + ) -> None: + super().__init__() + self.attn_norm = nn.RMSNorm(d_atom, elementwise_affine=False) + self.ffn_norm = nn.RMSNorm(d_atom, elementwise_affine=False) + + adaln_linear = nn.Linear(d_atom, 6 * d_atom, bias=False) + nn.init.zeros_(adaln_linear.weight) + self.adaln_modulation = nn.Sequential(nn.SiLU(), adaln_linear) + + self.attn = SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) + self.ffn = SwiGLUFFN(d_atom, expansion_ratio) + + self._rms_adaln = ( + torch.compile(_rms_adaln_raw) if use_compile_fusions else _rms_adaln_raw + ) + self._gated_residual = ( + torch.compile(_gated_residual_raw) + if use_compile_fusions + else _gated_residual_raw + ) + + def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: + mod = self.adaln_modulation(c_l) + if mod.dim() == 2: + mod = mod.unsqueeze(1) + shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) + + attn_input = self._rms_adaln(x, scale_a, shift_a) + attn_out = self.attn(attn_input, attention_params) + x = self._gated_residual(x, gate_a, attn_out) + + ffn_input = self._rms_adaln(x, scale_f, shift_f) + ffn_out = self.ffn(ffn_input) + x = self._gated_residual(x, gate_f, ffn_out) + return x + + +class SWAAtomTransformer(nn.Module): + """Stack of SWAAtomBlocks.""" + + def __init__( + self, + d_atom: int = 128, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.swa_window_size = swa_window_size + self.head_dim = d_atom // n_heads + self.spatial_rope_base_frequency = spatial_rope_base_frequency + self.n_spatial_rope_pairs_per_axis = n_spatial_rope_pairs_per_axis + self.n_uid_rope_pairs = n_uid_rope_pairs + self.uid_rope_base_frequency = uid_rope_base_frequency + + self.blocks = nn.ModuleList( + [ + SWAAtomBlock( + d_atom=d_atom, + n_heads=n_heads, + half_window=swa_window_size // 2, + expansion_ratio=expansion_ratio, + ) + for _ in range(n_blocks) + ] + ) + + def _build_3d_rope( + self, ref_pos: Tensor, ref_space_uid: Tensor + ) -> tuple[Tensor, Tensor]: + return build_3d_rope( + ref_pos=ref_pos, + ref_space_uid=ref_space_uid, + head_dim=self.head_dim, + n_spatial_per_axis=self.n_spatial_rope_pairs_per_axis, + n_uid_pairs=self.n_uid_rope_pairs, + spatial_base_freq=self.spatial_rope_base_frequency, + uid_base_freq=self.uid_rope_base_frequency, + ) + + def forward( + self, + q_l: Tensor, + c_l: Tensor, + attention_params: tuple, + return_intermediates: bool = False, + ) -> Tensor | tuple[Tensor, list[Tensor]]: + intermediates: list[Tensor] = [] + for block in self.blocks: + q_l = block(q_l, c_l, attention_params) + if return_intermediates: + intermediates.append(q_l) + if return_intermediates: + return q_l, intermediates + return q_l + + +# =========================================================================== +# ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) +# =========================================================================== + + +class ESMFold2AtomEncoder(nn.Module): + """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. + + Args: + d_atom: atom hidden dim + d_token: token dim for atom_to_token aggregation + n_blocks, n_heads, swa_window_size, expansion_ratio: transformer params + structure_prediction: if True, creates coords_linear and uses full d_token + spatial_rope_base_frequency, n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs, uid_rope_base_frequency: 3D RoPE config + """ + + def __init__( + self, + d_atom: int = 128, + d_token: int = 768, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + structure_prediction: bool = True, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.d_atom = d_atom + self.d_token = d_token + self.structure_prediction = structure_prediction + + self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) + self.atom_norm = nn.LayerNorm(d_atom) + + if structure_prediction: + self.coords_linear = nn.Linear(6, d_atom, bias=False) + + self.atom_transformer = SWAAtomTransformer( + d_atom=d_atom, + n_blocks=n_blocks, + n_heads=n_heads, + swa_window_size=swa_window_size, + expansion_ratio=expansion_ratio, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # Output aggregation: d_token for structure prediction, d_token//2 for inputs + out_dim = d_token if structure_prediction else d_token // 2 + self.atom_to_token_linear = nn.Linear(d_atom, out_dim, bias=False) + + def forward( + self, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + r_l: Tensor | None = None, + pred_r1: Tensor | None = None, + s_i: Tensor | None = None, + z_ij: Tensor | None = None, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + inference_cache: dict | None = None, + ) -> tuple[Tensor, Tensor, Tensor, tuple, list[Tensor]]: + """Returns (a, q, c, attention_params, intermediates). + + ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, + attention indices, n_tokens) across diffusion steps. + """ + B, N = ref_pos.shape[:2] + + layer_cache = None + if inference_cache is not None: + layer_cache = inference_cache.setdefault("atomencoder", {}) + + if layer_cache is None or len(layer_cache) == 0: + atom_feats = torch.cat( + [ + ref_pos, + ref_charge.unsqueeze(-1), + atom_attention_mask.unsqueeze(-1), + ref_element, + ref_atom_name_chars.reshape(B, N, MAX_CHARS * CHAR_VOCAB_SIZE), + ], + dim=-1, + ) + c_base = self.atom_norm(self.atom_linear(atom_feats)) + cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) + cos = cos.repeat_interleave(num_diffusion_samples, 0) + sin = sin.repeat_interleave(num_diffusion_samples, 0) + mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) + seqlens = mask_exp.sum(dim=-1, dtype=torch.int32) + indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() + max_seqlen = int(seqlens.max().item()) + cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) + attention_params = (cos, sin, indices, cu_seqlens, max_seqlen) + n_tokens = int(atom_to_token.max().item()) + 1 + if layer_cache is not None: + layer_cache["c_base"] = c_base + layer_cache["attention_params"] = attention_params + layer_cache["mask_exp"] = mask_exp + layer_cache["n_tokens"] = n_tokens + layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave( + num_diffusion_samples, 0 + ) + else: + c_base = layer_cache["c_base"] + attention_params = layer_cache["attention_params"] + mask_exp = layer_cache["mask_exp"] + n_tokens = layer_cache["n_tokens"] + + c = c_base + + q = c + + if self.structure_prediction and r_l is not None: + q = q.repeat_interleave(num_diffusion_samples, 0) + if pred_r1 is None: + pred_r1 = torch.zeros_like(r_l) + r_input = torch.cat([r_l, pred_r1], dim=-1) + r_to_q = self.coords_linear(r_input) + q = q + r_to_q + + c = c.repeat_interleave(num_diffusion_samples, 0) + + result = self.atom_transformer( + q_l=q, + c_l=c, + attention_params=attention_params, + return_intermediates=return_intermediates, + ) + if return_intermediates: + q, intermediates = result + else: + q = result + intermediates = [] + + q_to_a = F.relu(self.atom_to_token_linear(q)) + if layer_cache is not None and "atom_to_token_exp" in layer_cache: + atom_to_token_exp = layer_cache["atom_to_token_exp"] + else: + atom_to_token_exp = atom_to_token.repeat_interleave( + num_diffusion_samples, 0 + ) + a = scatter_atom_to_token( + q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool() + ) + + return a, q, c, attention_params, intermediates + + +# =========================================================================== +# ESMFold2AtomDecoder +# =========================================================================== + + +class ESMFold2AtomDecoder(nn.Module): + """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear.""" + + def __init__( + self, + d_atom: int = 128, + d_token: int = 768, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) + + self.atom_transformer = SWAAtomTransformer( + d_atom=d_atom, + n_blocks=n_blocks, + n_heads=n_heads, + swa_window_size=swa_window_size, + expansion_ratio=expansion_ratio, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + self.norm = nn.LayerNorm(d_atom) + self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) + + def forward( + self, + a_i: Tensor, + q_l: Tensor, + c_l: Tensor, + p_lm: tuple, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + ) -> tuple[Tensor, list[Tensor]]: + """Returns (r_update, intermediates).""" + atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + a_to_q = self.token_to_atom_linear(a_i) + a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) + q_l = q_l + a_to_q + + result = self.atom_transformer( + q_l=q_l, + c_l=c_l, + attention_params=p_lm, + return_intermediates=return_intermediates, + ) + if return_intermediates: + q_l, intermediates = result + else: + q_l = result + intermediates = [] + + r_l = self.output_linear(self.norm(q_l)) + return r_l, intermediates + + +# =========================================================================== +# AttentionPairBias (DiffusionTransformer attention block) +# =========================================================================== + + +class AttentionPairBias(nn.Module): + """Gated multi-head attention with pair bias conditioning.""" + + def __init__( + self, + d_model: int, + d_pair: int, + num_heads: int, + d_cond: int | None = None, + use_conditioning: bool = True, + ) -> None: + super().__init__() + self.d_model = d_model + self.num_heads = num_heads + self.head_dim = d_model // num_heads + self.scale = self.head_dim**-0.5 + d_cond = d_cond or d_model + + if use_conditioning: + self.adaln = AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + self.out_gate = nn.Linear(d_cond, d_model, bias=True) + # adaln init: weight=0, bias=-2 + nn.init.zeros_(self.out_gate.weight) + nn.init.constant_(self.out_gate.bias, -2.0) + else: + self.pre_norm = nn.LayerNorm(d_model, eps=1e-5) + + self.q_proj = nn.Linear(d_model, d_model, bias=True) + self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) + self.g_proj = nn.Linear(d_model, d_model, bias=False) + self.out_proj = nn.Linear(d_model, d_model, bias=False) + + if d_pair > 0: + self.pair_norm = nn.LayerNorm(d_pair, eps=1e-5) + self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) + + self._kernel_backend: str | None = None + + def set_kernel_backend(self, backend: str | None) -> None: + if backend not in _VALID_BACKENDS: + raise ValueError( + f"backend must be one of {_VALID_BACKENDS}, got {backend!r}" + ) + self._kernel_backend = backend + + def _is_zero_beta(self, beta: Tensor | float) -> bool: + if isinstance(beta, (int, float)): + return beta == 0.0 + return bool((beta == 0).all()) + + def _can_use_fused_pair_bias( + self, z: Tensor, n_queries: int, beta: Tensor | float + ) -> bool: + return ( + _fused_active(self, z) + and z.dim() == 4 + and self._is_zero_beta(beta) + and hasattr(self, "pair_bias_proj") + and hasattr(self, "pair_norm") + ) + + def _can_use_cueq_pair_bias( + self, z: Tensor, n_queries: int, beta: Tensor | float + ) -> bool: + return ( + _cueq_active(self) + and n_queries > 750 + and z.dim() == 4 + and self._is_zero_beta(beta) + and hasattr(self, "pair_bias_proj") + ) + + def forward( + self, + a: Tensor, + s: Tensor | None, + z: Tensor, + beta: Tensor | float = 0.0, + attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + ) -> Tensor: + bsz, n_queries, d_model = a.shape + + if s is not None: + x = self.adaln(a, s) + else: + x = self.pre_norm(a) + + n_keys = x.shape[1] + q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) + kv = self.kv_proj(x) + k, v = kv.chunk(2, dim=-1) + k = k.view(bsz, n_keys, self.num_heads, self.head_dim) + v = v.view(bsz, n_keys, self.num_heads, self.head_dim) + + # Expand z for num_diffusion_samples + if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: + z = z.repeat_interleave(num_diffusion_samples, dim=0) + if ( + attention_mask is not None + and attention_mask.shape[0] != bsz + and num_diffusion_samples > 1 + ): + attention_mask = attention_mask.repeat_interleave( + num_diffusion_samples, dim=0 + ) + + if self._can_use_fused_pair_bias(z, n_queries, beta): + kernel_mask = ( + attention_mask + if attention_mask is not None + else torch.ones(bsz, n_queries, device=a.device, dtype=torch.bool) + ) + pair_norm_w = self.pair_norm.weight + pair_norm_b = ( + self.pair_norm.bias + if self.pair_norm.bias is not None + else torch.zeros_like(pair_norm_w) + ) + z_bf = z if z.dtype == torch.bfloat16 else z.to(torch.bfloat16) + bias = _fused_pair_bias( # type: ignore[misc] + z_bf, + kernel_mask, + self.pair_bias_proj.weight, + num_heads=self.num_heads, + pair_norm_w=pair_norm_w, + pair_norm_b=pair_norm_b, + ) # (B, H, Q, K) + q_bhqd = q.transpose(1, 2) + k_bhqd = k.transpose(1, 2) + v_bhqd = v.transpose(1, 2) + attn_out = F.scaled_dot_product_attention( + q_bhqd, k_bhqd, v_bhqd, attn_mask=bias.to(q_bhqd.dtype) + ) + g = torch.sigmoid(self.g_proj(x)).view( + bsz, n_queries, self.num_heads, self.head_dim + ) + ctx = g * attn_out.transpose(1, 2) + out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) + if s is not None: + out = torch.sigmoid(self.out_gate(s)) * out + return out + + if self._can_use_cueq_pair_bias(z, n_queries, beta): + kernel_mask = ( + attention_mask + if attention_mask is not None + else torch.ones(bsz, n_queries, device=a.device, dtype=torch.bool) + ) + out, _ = _cue_attn_pair_bias( # type: ignore[misc] + s=x, + q=q.transpose(1, 2), + k=k.transpose(1, 2), + v=v.transpose(1, 2), + z=z, + mask=kernel_mask, + num_heads=self.num_heads, + w_proj_z=self.pair_bias_proj.weight, + w_proj_g=self.g_proj.weight, + w_proj_o=self.out_proj.weight, + w_ln_z=self.pair_norm.weight, + b_ln_z=self.pair_norm.bias, + return_z_proj=False, + is_cached_z_proj=False, + ) + else: + # Standard attention with pair bias + g = torch.sigmoid(self.g_proj(x)).view( + bsz, n_queries, self.num_heads, self.head_dim + ) + + logits = ( + torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale + ) + + if z.dim() == 4: + pair_bias = self.pair_bias_proj(self.pair_norm(z)) + else: + pair_bias = z.unsqueeze(-1) + logits = logits + pair_bias.to(dtype=logits.dtype) + + if attention_mask is not None: + min_val = torch.finfo(logits.dtype).min + mask_bias = torch.where( + attention_mask.bool()[:, None, :, None], 0.0, min_val + ) + logits = logits + mask_bias.to(dtype=logits.dtype) + + attn = torch.softmax(logits, dim=-2).to(dtype=v.dtype) + ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) + ctx = g * ctx + out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) + + if s is not None: + out = torch.sigmoid(self.out_gate(s)) * out + return out + + +# =========================================================================== +# ConditionedTransitionBlock +# =========================================================================== + + +class ConditionedTransitionBlock(nn.Module): + """Conditioned SwiGLU transition with adaptive layer norm.""" + + def __init__( + self, + d_model: int, + d_cond: int | None = None, + transition_multiplier: int = 2, + use_conditioning: bool = True, + ) -> None: + super().__init__() + d_cond = d_cond or d_model + hidden = transition_multiplier * d_model + + if use_conditioning: + self.adaln = AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + self.output_gate = nn.Linear(d_cond, d_model, bias=True) + nn.init.zeros_(self.output_gate.weight) + nn.init.constant_(self.output_gate.bias, -2.0) + else: + self.pre_norm = nn.LayerNorm(d_model, eps=1e-5) + + self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) + self.lin_out = nn.Linear(hidden, d_model, bias=False) + + def forward(self, a: Tensor, s: Tensor | None) -> Tensor: + if s is not None: + x = self.adaln(a, s) + else: + x = self.pre_norm(a) + + swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) + b = F.silu(swish_a) * swish_b + out = self.lin_out(b) + + if s is not None: + out = torch.sigmoid(self.output_gate(s)) * out + return out + + +# =========================================================================== +# DiffusionTransformer (token transformer) +# =========================================================================== + + +class DiffusionTransformer(nn.Module): + """Diffusion denoising transformer with attention pair bias.""" + + def __init__( + self, + d_model: int, + d_pair: int, + num_heads: int, + num_blocks: int, + d_cond: int | None = None, + transition_multiplier: int = 2, + use_conditioning: bool = True, + ) -> None: + super().__init__() + d_cond = d_cond or d_model + + self.attn_blocks = nn.ModuleList( + [ + AttentionPairBias( + d_model=d_model, + d_pair=d_pair, + num_heads=num_heads, + d_cond=d_cond, + use_conditioning=use_conditioning, + ) + for _ in range(num_blocks) + ] + ) + self.transition_blocks = nn.ModuleList( + [ + ConditionedTransitionBlock( + d_model=d_model, + d_cond=d_cond, + transition_multiplier=transition_multiplier, + use_conditioning=use_conditioning, + ) + for _ in range(num_blocks) + ] + ) + + def set_kernel_backend(self, backend: str | None) -> None: + for attn in self.attn_blocks: + cast(AttentionPairBias, attn).set_kernel_backend(backend) + + def forward( + self, + a: Tensor, + s: Tensor | None, + z: Tensor, + beta: Tensor | float = 0.0, + attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + ) -> tuple[Tensor, list[Tensor]]: + intermediates: list[Tensor] = [] + x = a + for attn, transition in zip(self.attn_blocks, self.transition_blocks): + x = x + attn( + x, + s, + z, + beta, + attention_mask=attention_mask, + num_diffusion_samples=num_diffusion_samples, + ) + x = x + transition(x, s) + if return_intermediates: + intermediates.append(x) + return x, intermediates + + +# =========================================================================== +# DiffusionConditioning +# =========================================================================== + + +class DiffusionConditioning(nn.Module): + """Conditions pair and single representations on noise timestep.""" + + def __init__( + self, + c_z: int = 256, + c_s: int = 768, + c_s_inputs: int = 451, + sigma_data: float = 16.0, + fourier_dim: int = 256, + transition_multiplier: int = 2, + layer_norm_eps: float = 1e-5, + ) -> None: + super().__init__() + self.sigma_data = float(sigma_data) + self.c_z = c_z + self.c_s = c_s + self.c_s_inputs = c_s_inputs + + self.z_input_norm = nn.LayerNorm(2 * c_z, eps=layer_norm_eps) + self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) + self.z_transitions = nn.ModuleList( + [ + TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) + for _ in range(2) + ] + ) + + self.s_input_norm = nn.LayerNorm(c_s_inputs, eps=layer_norm_eps) + self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) + self.fourier = FourierEmbedding(fourier_dim) + self.noise_norm = nn.LayerNorm(fourier_dim, eps=layer_norm_eps) + self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) + self.s_transitions = nn.ModuleList( + [ + TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) + for _ in range(2) + ] + ) + + def forward( + self, + t_hat: Tensor, + s_inputs: Tensor, + s_trunk: Tensor | None, + z_trunk: Tensor, + relative_position_encoding: Tensor, + sigma_data: float | None = None, + num_diffusion_samples: int = 1, + inference_cache: dict[str, Tensor] | None = None, + ) -> tuple[Tensor, Tensor]: + sigma = self.sigma_data if sigma_data is None else float(sigma_data) + base_batch = z_trunk.shape[0] + target_batch = base_batch * num_diffusion_samples + + # z conditioning (cached across diffusion steps — independent of t_hat) + if inference_cache is not None and "z" in inference_cache: + z = inference_cache["z"] + else: + z_rel = relative_position_encoding.to(dtype=torch.float32) + z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) + z = self.z_proj(self.z_input_norm(z)) + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + for block in self.z_transitions: + z = z + block(z) + if inference_cache is not None: + inference_cache["z"] = z + + # s conditioning + s_inputs_eff = s_inputs + if s_inputs_eff.shape[0] != target_batch: + s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) + + s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32))) + + # Noise embedding + t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) + if t.numel() == 1: + t = t.expand(target_batch) + elif t.shape[0] != target_batch: + t = t.repeat_interleave(num_diffusion_samples, 0) + t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) + n = self.fourier(t_noise) + n = self.noise_proj(self.noise_norm(n)) + s = s + n.unsqueeze(1) + + for block in self.s_transitions: + s = s + block(s) + + return s, z + + +# =========================================================================== +# DiffusionModule +# =========================================================================== + + +class DiffusionModule(nn.Module): + """Diffusion denoising module for structure prediction.""" + + def __init__( + self, + c_atom: int = 128, + c_token: int = 768, + c_z: int = 256, + c_s_inputs: int = 451, + sigma_data: float = 16.0, + fourier_dim: int = 256, + atom_num_blocks: int = 3, + atom_num_heads: int = 4, + token_num_blocks: int = 12, + token_num_heads: int = 16, + transition_multiplier: int = 2, + swa_window_size: int = 128, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.sigma_data = float(sigma_data) + + self.conditioning = DiffusionConditioning( + c_z=c_z, + c_s=c_token, # conditioning s output is c_token + c_s_inputs=c_s_inputs, + sigma_data=sigma_data, + fourier_dim=fourier_dim, + transition_multiplier=transition_multiplier, + ) + + # Atom encoder (structure_prediction=True, with coords_linear) + self.atom_encoder = ESMFold2AtomEncoder( + d_atom=c_atom, + d_token=c_token, + n_blocks=atom_num_blocks, + n_heads=atom_num_heads, + swa_window_size=swa_window_size, + expansion_ratio=2, + structure_prediction=True, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # Atom decoder + self.atom_decoder = ESMFold2AtomDecoder( + d_atom=c_atom, + d_token=c_token, + n_blocks=atom_num_blocks, + n_heads=atom_num_heads, + swa_window_size=swa_window_size, + expansion_ratio=2, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + self.s_to_token = nn.Linear(c_token, c_token, bias=False) + nn.init.zeros_(self.s_to_token.weight) + + # Token transformer (DiffusionTransformer with pair bias) + self.token_transformer = DiffusionTransformer( + d_model=c_token, + d_pair=c_z, + num_heads=token_num_heads, + num_blocks=token_num_blocks, + d_cond=c_token, + transition_multiplier=transition_multiplier, + use_conditioning=True, + ) + + self.s_step_norm = nn.LayerNorm(c_token) + self.token_norm = nn.LayerNorm(c_token) + + def set_kernel_backend(self, backend: str | None) -> None: + self.token_transformer.set_kernel_backend(backend) + + def forward( + self, + x_noisy: Tensor, + t_hat: Tensor, + ref_pos: Tensor, + ref_charge: Tensor, + ref_mask: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + tok_idx: Tensor, + s_inputs: Tensor, + s_trunk: Tensor | None, + z_trunk: Tensor, + relative_position_encoding: Tensor, + asym_id: Tensor, + residue_index: Tensor, + entity_id: Tensor, + token_index: Tensor, + sym_id: Tensor, + sigma_data: float | None = None, + token_attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + return_token_repr: bool = False, + return_atom_repr: bool = False, + inference_cache: dict[str, Tensor] | None = None, + ) -> dict[str, Tensor | None]: + bsz = x_noisy.shape[0] + sigma = self.sigma_data if sigma_data is None else float(sigma_data) + t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape( + -1 + ) + if t.numel() == 1: + t = t.expand(bsz) + + # Step 1: conditioning (pair z is cached across diffusion steps) + s, z = self.conditioning( + t_hat=t, + s_inputs=s_inputs, + s_trunk=s_trunk, + z_trunk=z_trunk, + relative_position_encoding=relative_position_encoding, + sigma_data=sigma, + num_diffusion_samples=num_diffusion_samples, + inference_cache=inference_cache, + ) + + # Step 2: normalize noisy coords + denom = torch.sqrt(t * t + sigma * sigma) + r_noisy = x_noisy / denom[:, None, None] + + # Step 3: atom encoder + a, q_skip, c_skip, p_skip, enc_intermediates = self.atom_encoder( + ref_pos=ref_pos, + atom_attention_mask=ref_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=tok_idx, + r_l=r_noisy, + s_i=s_trunk, + num_diffusion_samples=num_diffusion_samples, + return_intermediates=return_atom_repr, + inference_cache=inference_cache, + ) + + # Step 4: add conditioned s + a = a + self.s_to_token(self.s_step_norm(s)) + + # Step 5: token transformer + a, _ = self.token_transformer( + a, + s, + z, + beta=0.0, + attention_mask=token_attention_mask, + num_diffusion_samples=num_diffusion_samples, + ) + + # Step 6: token norm + a = self.token_norm(a) + + # Step 7: atom decoder + r_update, dec_intermediates = self.atom_decoder( + a_i=a, + q_l=q_skip, + c_l=c_skip, + p_lm=p_skip, + atom_to_token=tok_idx, + atom_attention_mask=ref_mask, + num_diffusion_samples=num_diffusion_samples, + return_intermediates=return_atom_repr, + ) + + # Step 8: compute denoised output + sigma2 = sigma * sigma + t2 = t * t + out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy + out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update + + # Collect atom intermediates from encoder + decoder + atom_intermediates: Tensor | None = None + if return_atom_repr: + all_ints = enc_intermediates + dec_intermediates + if all_ints: + atom_intermediates = torch.stack(all_ints, dim=2) + + return { + "x_denoised": out, + "token_repr": a if return_token_repr else None, + "atom_intermediates": atom_intermediates, + } + + +# =========================================================================== +# DiffusionStructureHead +# =========================================================================== + + +class DiffusionStructureHead(nn.Module): + """Wrapper around DiffusionModule with diffusion sampling.""" + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + dm = config.structure_head.diffusion_module + swa_cfg = config.inputs.atom_encoder + sh = config.structure_head + + self.diffusion_module = DiffusionModule( + c_atom=dm.c_atom, + c_token=dm.c_token, + c_z=dm.c_z, + c_s_inputs=dm.c_s_inputs, + sigma_data=dm.sigma_data, + fourier_dim=dm.fourier_dim, + atom_num_blocks=dm.atom_num_blocks, + atom_num_heads=dm.atom_num_heads, + token_num_blocks=dm.token_num_blocks, + token_num_heads=dm.token_num_heads, + transition_multiplier=dm.transition_multiplier, + swa_window_size=swa_cfg.swa_window_size, + spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, + uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, + ) + + # Sampling hyperparameters + self.sigma_data = dm.sigma_data + self.gamma_0 = sh.gamma_0 + self.gamma_min = sh.gamma_min + self.noise_scale = sh.noise_scale + self.step_scale = sh.step_scale + self.inference_s_max = sh.inference_s_max + self.inference_s_min = sh.inference_s_min + self.inference_p = sh.inference_p + self.inference_num_steps = sh.inference_num_steps + + def set_kernel_backend(self, backend: str | None) -> None: + self.diffusion_module.set_kernel_backend(backend) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def inference_noise_schedule( + self, num_steps: int | None = None, device: torch.device | None = None + ) -> Tensor: + """Karras power-law noise schedule.""" + steps = self.inference_num_steps if num_steps is None else int(num_steps) + if steps == 1: + return torch.tensor( + [self.inference_s_max * self.sigma_data, 0.0], + device=device, + dtype=torch.float32, + ) + p = float(self.inference_p) + inv_p = 1.0 / p + k = torch.arange(steps, device=device, dtype=torch.float32) + base = self.inference_s_max**inv_p + (k / (steps - 1)) * ( + self.inference_s_min**inv_p - self.inference_s_max**inv_p + ) + schedule = self.sigma_data * base.pow(p) + return F.pad(schedule, (0, 1), value=0.0) + + @staticmethod + def _random_rotations(n: int, dtype: torch.dtype, device: torch.device) -> Tensor: + q = torch.randn((n, 4), dtype=dtype, device=device) + scale = torch.sqrt((q * q).sum(dim=1)) + signs = torch.where(q[:, 0] < 0, -scale, scale) + q = q / signs[:, None] + r, i, j, k = torch.unbind(q, dim=-1) + two_s = 2.0 / (q * q).sum(dim=-1) + return torch.stack( + ( + 1 - two_s * (j * j + k * k), + two_s * (i * j - k * r), + two_s * (i * k + j * r), + two_s * (i * j + k * r), + 1 - two_s * (i * i + k * k), + two_s * (j * k - i * r), + two_s * (i * k - j * r), + two_s * (j * k + i * r), + 1 - two_s * (i * i + j * j), + ), + dim=-1, + ).reshape(n, 3, 3) + + def _center_random_augmentation( + self, x: Tensor, atom_mask: Tensor, second_coords: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + """Algorithm 19: center + random rotation + translation.""" + bsz = x.shape[0] + mask = atom_mask.unsqueeze(-1) # [B, A, 1] + denom = mask.sum(dim=1, keepdim=True).clamp(min=1) + mean = (x * mask).sum(dim=1, keepdim=True) / denom + x = x - mean + if second_coords is not None: + second_coords = second_coords - mean + + r = self._random_rotations(bsz, x.dtype, x.device) + x = torch.einsum("bmd,bds->bms", x, r) + if second_coords is not None: + second_coords = torch.einsum("bmd,bds->bms", second_coords, r) + + t = torch.randn_like(x[:, 0:1, :]) + x = x + t + if second_coords is not None: + second_coords = second_coords + t + return x, second_coords + + @staticmethod + def _weighted_rigid_align( + x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor + ) -> Tensor: + """Kabsch alignment: align x to x_gt with weights w.""" + w = (mask * w).unsqueeze(-1) # [B, N, 1] + denom = w.sum(dim=-2, keepdim=True).clamp(min=1e-8) + mu = (x * w).sum(dim=-2, keepdim=True) / denom + mu_gt = (x_gt * w).sum(dim=-2, keepdim=True) / denom + x_c = x - mu + xgt_c = x_gt - mu_gt + H = torch.einsum("bni,bnj->bij", w * xgt_c, x_c) + H32 = H.float() + U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) + det = torch.linalg.det(U @ Vh) + ones = torch.ones_like(det) + R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to( + H.dtype + ) + return x_c @ R.transpose(-1, -2) + mu_gt + + # ------------------------------------------------------------------ + # Sampling + # ------------------------------------------------------------------ + + @torch.inference_mode() + def sample( + self, + z_trunk: Tensor, + s_inputs: Tensor, + s_trunk: Tensor | None, + relative_position_encoding: Tensor, + ref_pos: Tensor, + ref_charge: Tensor, + ref_mask: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + tok_idx: Tensor, + asym_id: Tensor, + residue_index: Tensor, + entity_id: Tensor, + token_index: Tensor, + sym_id: Tensor, + token_attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + num_sampling_steps: int | None = None, + max_inference_sigma: float | None = 256.0, + noise_scale: float | None = None, + step_scale: float | None = None, + return_atom_repr: bool = False, + use_inference_cache: bool = True, + denoising_early_exit_rmsd: float | None = None, + ) -> dict[str, Tensor | None]: + """Diffusion sampling (Algorithm 18). + + ``num_sampling_steps`` is the number of denoising steps actually run. + When ``max_inference_sigma`` is set, the Karras schedule built with + ``num_sampling_steps`` entries would lose its high-σ tail to the cap, + so we inflate the underlying schedule length here to land back at the + requested step count post-truncation. + """ + n_atoms = tok_idx.shape[1] + device = s_inputs.device + target_batch = s_inputs.shape[0] * num_diffusion_samples + + inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None + + steps = ( + self.inference_num_steps + if num_sampling_steps is None + else int(num_sampling_steps) + ) + + schedule = self.inference_noise_schedule(steps, device) + if max_inference_sigma is not None: + schedule = schedule[schedule <= float(max_inference_sigma)] + schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) + + lam = self.noise_scale if noise_scale is None else float(noise_scale) + eta = self.step_scale if step_scale is None else float(step_scale) + + x = schedule[0] * torch.randn( + target_batch, n_atoms, 3, device=device, dtype=torch.float32 + ) + atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() + + gammas = torch.where( + schedule > self.gamma_min, + torch.full_like(schedule, self.gamma_0), + torch.zeros_like(schedule), + ) + + x_denoised_prev: Tensor | None = None + token_repr: Tensor | None = None + diff_atom_intermediates: Tensor | None = None + + step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) + num_steps = len(step_pairs) + + for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): + x, x_denoised_prev = self._center_random_augmentation( + x, atom_mask, second_coords=x_denoised_prev + ) + + sigma_tm_val = float(sigma_tm.item()) + t_hat_val = sigma_tm_val * (1.0 + float(gamma.item())) + eps_std = lam * max(t_hat_val**2 - sigma_tm_val**2, 0.0) ** 0.5 + x_noisy = x + eps_std * torch.randn_like(x) + + is_last_step = step_idx == num_steps - 1 + request_atom_repr = return_atom_repr and ( + is_last_step or denoising_early_exit_rmsd is not None + ) + + dm_out = self.diffusion_module( + x_noisy=x_noisy, + t_hat=torch.full( + (target_batch,), t_hat_val, device=device, dtype=torch.float32 + ), + ref_pos=ref_pos, + ref_charge=ref_charge, + ref_mask=ref_mask, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + ref_space_uid=ref_space_uid, + tok_idx=tok_idx, + s_inputs=s_inputs, + s_trunk=s_trunk, + z_trunk=z_trunk, + relative_position_encoding=relative_position_encoding, + asym_id=asym_id, + residue_index=residue_index, + entity_id=entity_id, + token_index=token_index, + sym_id=sym_id, + token_attention_mask=token_attention_mask, + num_diffusion_samples=num_diffusion_samples, + return_token_repr=True, + return_atom_repr=request_atom_repr, + inference_cache=inference_cache, + ) + + x_denoised = dm_out["x_denoised"] + token_repr = dm_out["token_repr"] + if request_atom_repr: + diff_atom_intermediates = dm_out.get("atom_intermediates") + + # Reverse diffusion alignment (Kabsch) + with torch.autocast(device_type="cuda", enabled=False): + x_noisy = self._weighted_rigid_align( + x_noisy.float(), x_denoised.float(), atom_mask, atom_mask + ) + x_noisy = x_noisy.to(dtype=x_denoised.dtype) + + # ODE/SDE step + sigma_t_val = float(sigma_t.item()) + denoised_over_sigma = (x_noisy - x_denoised) / t_hat_val + x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma + + # Denoising early-exit: stop when consecutive predictions converge + if ( + denoising_early_exit_rmsd is not None + and x_denoised_prev is not None + and step_idx >= 1 + ): + with torch.autocast(device_type="cuda", enabled=False): + aligned = self._weighted_rigid_align( + x_denoised_prev.float(), + x_denoised.float(), + atom_mask, + atom_mask, + ) + diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) + per_sample_rmsd = ( + diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1) + ).sqrt() + if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: + x = x_denoised + x_denoised_prev = x_denoised + break + + x_denoised_prev = x_denoised + + result: dict[str, Tensor | None] = { + "sample_atom_coords": x, + "diff_token_repr": token_repr, + } + if return_atom_repr: + result["diff_atom_intermediates"] = diff_atom_intermediates + return result + + +# =========================================================================== +# RowAttentionPooling +# =========================================================================== + + +class RowAttentionPooling(nn.Module): + """Row-wise attention pooling: attn_proj, out_proj.""" + + def __init__(self, d_pair: int, d_single: int) -> None: + super().__init__() + self.attn_proj = nn.Linear(d_pair, 1, bias=False) + self.out_proj = nn.Linear(d_pair, d_single, bias=False) + + def forward(self, z: Tensor, mask: Tensor) -> Tensor: + scores = self.attn_proj(z).squeeze(-1) + mask_bias = torch.where( + mask[:, None, :].bool(), + torch.zeros_like(scores), + torch.full_like(scores, -1e9), + ) + scores = scores + mask_bias + weights = F.softmax(scores, dim=-1) + pooled = torch.einsum("bnm,bnmd->bnd", weights, z) + return self.out_proj(pooled) + + +# =========================================================================== +# InputsEmbedder +# =========================================================================== + + +class InputsEmbedder(nn.Module): + """Embeds input features including atom-level encoding via SWA attention.""" + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + swa_cfg = config.inputs.atom_encoder + + self.atom_attention_encoder = ESMFold2AtomEncoder( + d_atom=swa_cfg.d_atom, + d_token=swa_cfg.d_token, + n_blocks=swa_cfg.n_blocks, + n_heads=swa_cfg.n_heads, + swa_window_size=swa_cfg.swa_window_size, + expansion_ratio=swa_cfg.expansion_ratio, + structure_prediction=False, # no coords_linear + spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, + uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, + ) + + def forward( + self, + aatype: Tensor, + profile: Tensor, + deletion_mean: Tensor, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + ) -> Tensor: + """Embed inputs into per-token features. + + Returns: + [B, L, d_inputs] concatenation of atom encoding, aatype, profile, + and deletion_mean. + """ + a, _q, _c, _attn_params, _intermediates = self.atom_attention_encoder( + ref_pos=ref_pos, + atom_attention_mask=atom_attention_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=atom_to_token, + ) + return torch.cat([a, aatype, profile, deletion_mean.unsqueeze(-1)], dim=-1) + + +# =========================================================================== +# ResIdxAsymIdSymIdEntityIdEncoding (trunk relative position) +# =========================================================================== + + +class ResIdxAsymIdSymIdEntityIdEncoding(nn.Module): + """embed.weight [d_pair, n_features] where n_features = 2*(2*r_bins+2) + 1 + (2*c_bins+2). + + For default r_bins=32, c_bins=2: 2*66 + 1 + 6 = 139. + """ + + def __init__( + self, + n_relative_residx_bins: int = 32, + n_relative_chain_bins: int = 2, + d_pair: int = 256, + ) -> None: + super().__init__() + self.n_relative_residx_bins = n_relative_residx_bins + self.n_relative_chain_bins = n_relative_chain_bins + self.d_pair = d_pair + + n_feats_residue = 2 * n_relative_residx_bins + 2 + n_feats_token = 2 * n_relative_residx_bins + 2 + n_feats_chain = 2 * n_relative_chain_bins + 2 + n_feats_same_entity = 1 + total_feats = ( + n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity + ) + self.embed = nn.Linear(total_feats, d_pair, bias=False) + + def forward( + self, + residue_index: Tensor, + asym_id: Tensor, + sym_id: Tensor, + entity_id: Tensor, + token_index: Tensor, + ) -> Tensor: + bij_same_chain = asym_id.unsqueeze(2) == asym_id.unsqueeze(1) + bij_same_residue = residue_index.unsqueeze(2) == residue_index.unsqueeze(1) + bij_same_entity = entity_id.unsqueeze(2) == entity_id.unsqueeze(1) + + dij_residue = residue_index.unsqueeze(2) - residue_index.unsqueeze(1) + dij_residue = torch.clip( + dij_residue + self.n_relative_residx_bins, + 0, + 2 * self.n_relative_residx_bins, + ) + dij_residue = torch.where( + bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1 + ) + aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) + + dij_token = torch.clip( + token_index.unsqueeze(2) + - token_index.unsqueeze(1) + + self.n_relative_residx_bins, + 0, + 2 * self.n_relative_residx_bins, + ) + dij_token = torch.where( + bij_same_chain & bij_same_residue, + dij_token, + 2 * self.n_relative_residx_bins + 1, + ) + aij_rel_token = F.one_hot(dij_token, 2 * self.n_relative_residx_bins + 2) + + dij_chain = torch.clip( + sym_id.unsqueeze(2) - sym_id.unsqueeze(1) + self.n_relative_chain_bins, + 0, + 2 * self.n_relative_chain_bins, + ) + dij_chain = torch.where( + bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain + ) + aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) + + feats = torch.cat( + [ + aij_rel_pos.float(), + aij_rel_token.float(), + bij_same_entity.float().unsqueeze(-1), + aij_rel_chain.float(), + ], + dim=-1, + ) + + return self.embed(feats) + + +# =========================================================================== +# SingleToPair (for LanguageModelShim) +# =========================================================================== + + +class SingleToPair(nn.Module): + """downproject, output_mlp (Sequential of Linear, GELU, Linear).""" + + def __init__(self, input_dim: int, downproject_dim: int, output_dim: int) -> None: + super().__init__() + self.downproject = nn.Linear(input_dim, downproject_dim) + self.output_mlp = nn.Sequential( + nn.Linear(2 * downproject_dim, output_dim), + nn.GELU(), + nn.Linear(output_dim, output_dim), + ) + + def forward(self, x: Tensor) -> Tensor: + x = self.downproject(x) + x = torch.cat( + [(x.unsqueeze(2) * x.unsqueeze(1)), (x.unsqueeze(2) - x.unsqueeze(1))], + dim=3, + ) + return self.output_mlp(x) + + +# =========================================================================== +# LanguageModelShim +# =========================================================================== + + +class LanguageModelShim(nn.Module): + """Shim holding the trainable projection weights for LM integration. + + Contains: + - base_z_combine: nn.Parameter [num_layers+1] + - base_z_linear: Sequential(LayerNorm(d_model), Linear(d_model, d_z, bias=False)) + - base_z_mlp: Sequential(SingleToPair(d_z, d_z, d_z), LayerNorm(d_z)) + """ + + def __init__( + self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80 + ) -> None: + super().__init__() + + self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z)) + self.base_z_linear = nn.Sequential( + nn.LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False) + ) + self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) + + def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: + """Project pre-computed ESMC hidden states to pair representation. + + Args: + hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. + lm_dropout: Dropout probability applied to the pair + representation after ``base_z_mlp``. + + Returns: + [B, L, L, d_pair] pair representation. + """ + lm_z = self.base_z_linear(hidden_states) # [B, L, 81, d_z] + weights = self.base_z_combine.softmax(0) # [81] + lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] + lm_z = self.base_z_mlp(lm_z) # [B, L, L, d_z] + if lm_dropout > 0: + lm_z = F.dropout(lm_z, p=lm_dropout, training=True) + return lm_z + + +# =========================================================================== +# Reproducibility helper (mirrors evolutionaryscale.utils.reproducibility) +# =========================================================================== + + +@contextmanager +def _seed_context(seed: int | None, *, cuda: bool = True): + """Temporarily seed Python, NumPy, and PyTorch RNGs.""" + if seed is None: + yield + return + py_state = random.getstate() + np_state = np.random.get_state() + torch_state = torch.get_rng_state() + cuda_states = ( + torch.cuda.get_rng_state_all() if cuda and torch.cuda.is_available() else None + ) + seed = int(seed) % (2**32) + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + if cuda and torch.cuda.is_available(): + torch.cuda.manual_seed_all(seed) + try: + yield + finally: + random.setstate(py_state) + np.random.set_state(np_state) + torch.set_rng_state(torch_state) + if cuda_states is not None: + torch.cuda.set_rng_state_all(cuda_states) + + +# =========================================================================== +# ESMFold2ExperimentalModel — the top-level PreTrainedModel +# =========================================================================== + + +def compute_lm_hidden_states( + esmc: nn.Module, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + token_mask: Tensor, + pad_to_multiple: int | None = None, +) -> Tensor: + """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. + + Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple + structure tokens but share a single ``(asym_id, residue_index)`` key — + collapse them to one LM token per residue before running the LM (the LM + was trained on per-residue inputs, not per-atom), then scatter the + hidden states back to the per-token layout. + """ + B, L = input_ids.shape + device = input_ids.device + protein_mask = (mol_type == 0) & token_mask + + lm_input_list = [] + lm_lengths = [] + # Per-batch maps from (original protein-token index) to (LM input position). + expand_maps: list[Tensor] = [] + for b in range(B): + mask_b = protein_mask[b] + ids_b = input_ids[b][mask_b] + asym_b = asym_id[b][mask_b] + res_b = residue_index[b][mask_b] + + # Collapse: keep first token per (asym_id, residue_index) key, in + # input order. ``inverse`` maps each original protein-token to its + # collapsed residue index. + keys = torch.stack((asym_b, res_b), dim=1) + unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) + n_unique = unique_keys.size(0) + token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) + first_pos = torch.full( + (n_unique,), keys.size(0), device=device, dtype=torch.long + ) + first_pos.scatter_reduce_( + 0, inverse, token_positions, reduce="amin", include_self=True + ) + ordered = torch.argsort(first_pos) + first_pos_ordered = first_pos[ordered] + ids_collapsed = ids_b[first_pos_ordered] + asym_collapsed = asym_b[first_pos_ordered] + remap = torch.empty_like(ordered) + remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) + inverse_ordered = remap[inverse] + + chain_ids = asym_collapsed.unique(sorted=True) + # [BOS] chain1 [EOS BOS] chain2 ... [EOS] + parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] + # Per-chain LM positions accumulate; track them for the expand map. + per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) + cursor = 1 # position 0 is the leading BOS + for i, cid in enumerate(chain_ids): + in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] + parts.append(ids_collapsed[in_chain]) + per_token_lm_pos[in_chain] = torch.arange( + cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long + ) + cursor += in_chain.shape[0] + if i < len(chain_ids) - 1: + parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) + cursor += 2 # EOS + BOS + parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) + lm_seq = torch.cat(parts) + lm_input_list.append(lm_seq) + lm_lengths.append(lm_seq.shape[0]) + + # Original protein-token position → LM input position. + prot_pos_b = mask_b.nonzero(as_tuple=True)[0] + expand_map = torch.full((L,), -1, device=device, dtype=torch.long) + expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] + expand_maps.append(expand_map) + + # Pad to longest LM input; round to ``pad_to_multiple`` when fp8 is on + # (TE fp8 kernels assert prod(shape[:-1]) % 8 == 0). + max_len = max(lm_lengths) + if pad_to_multiple is not None and pad_to_multiple > 1: + max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple + lm_input_ids = torch.full( + (B, max_len), + 1, + device=device, + dtype=input_ids.dtype, # PAD=1 + ) + for b in range(B): + lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] + + # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). + sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 + sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 + + with torch.inference_mode(): + esmc_out = esmc( + input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True + ) + + hs = esmc_out.hidden_states # [n_layers+1, B, max_len, D] + n_layers_plus_1, _, _, D = hs.shape + result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) + for b in range(B): + mb = protein_mask[b] + em = expand_maps[b][mb] # [n_protein_tokens] LM positions + # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] + gathered = hs[:, b, em, :].permute(1, 0, 2) + result[b, mb.nonzero(as_tuple=True)[0]] = gathered + + return result.detach() + + +# =========================================================================== +# TriangleMultiplicativeUpdate +# =========================================================================== +class TriangleMultiplicativeBlock(nn.Module): + """Triangle multiplicative update block with gated signal routing.""" + + _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} + _VALID_FLOWS = ("outgoing", "incoming") + + def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None: + super().__init__() + if flow not in self._FLOW_TO_EINSUM: + raise ValueError( + f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}." + ) + + self.input_channels = input_channels + self.latent_channels = latent_channels + self.flow = flow + self._einsum_equation = self._FLOW_TO_EINSUM[flow] + self.norm_start = nn.LayerNorm(self.input_channels, eps=_EPS) + self.norm_mix = nn.LayerNorm(self.latent_channels, eps=_EPS) + self.proj_bundle = nn.Linear( + self.input_channels, 4 * self.latent_channels, bias=False + ) + self.proj_emit = nn.Linear( + self.latent_channels, self.input_channels, bias=False + ) + self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) + + self._use_kernels: bool = False + # Default chunked for memory on long sequences; tests override with + # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 + # parity checks. + self._chunk_size: int | None = 64 + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def split_kernel_weights(self) -> tuple[Tensor, Tensor]: + return ( + self.proj_bundle.weight[: 2 * self.latent_channels, :], + self.proj_bundle.weight[2 * self.latent_channels :, :], + ) + + def _kernel_flow_direction(self) -> str: + return self.flow + + def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: + return torch.einsum(self._einsum_equation, left_stream, right_stream) + + def _triangular_contract_chunked( + self, left_stream: Tensor, right_stream: Tensor, chunk_size: int + ) -> Tensor: + """Compute the triangular einsum in chunks along the output i-dimension.""" + L = left_stream.shape[1] if self.flow == "outgoing" else left_stream.shape[2] + chunks = [] + for start in range(0, L, chunk_size): + end = min(start + chunk_size, L) + if self.flow == "outgoing": + chunk = torch.einsum( + self._einsum_equation, left_stream[:, start:end], right_stream + ) + else: + chunk = torch.einsum( + self._einsum_equation, left_stream[:, :, start:end], right_stream + ) + chunks.append(chunk) + return torch.cat(chunks, dim=1) + + def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor: + if visibility is None: + visibility = pair_grid.new_ones(pair_grid.shape[:-1]) + + if self._use_kernels: + p_in_weight, g_in_weight = self.split_kernel_weights() + + try: + return _cue_tri_mul( # type: ignore[misc] + pair_grid, + direction=self._kernel_flow_direction(), + mask=visibility, + norm_in_weight=self.norm_start.weight, + norm_in_bias=self.norm_start.bias, + p_in_weight=p_in_weight, + g_in_weight=g_in_weight, + norm_out_weight=self.norm_mix.weight, + norm_out_bias=self.norm_mix.bias, + p_out_weight=self.proj_emit.weight, + g_out_weight=self.proj_gate.weight, + eps=_EPS, + ) + except Exception as e: + import logging as _logging + + _logging.getLogger(__name__).warning( + "cuequivariance triangle_multiplicative_update kernel failed " + "(flow=%s, shape=%s, dtype=%s); falling back to chunked einsum. " + "Error: %s", + self.flow, + tuple(pair_grid.shape), + pair_grid.dtype, + e, + ) + + normalized_grid = self.norm_start(pair_grid) + bundled = self.proj_bundle(normalized_grid) + signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) + routed = signal * torch.sigmoid(gate_logits) + routed = routed * visibility.unsqueeze(-1) + + left_stream, right_stream = routed.float().chunk(2, dim=-1) + if self._chunk_size is not None: + contracted = self._triangular_contract_chunked( + left_stream, right_stream, self._chunk_size + ) + else: + contracted = self._triangular_contract(left_stream, right_stream) + mixed = self.proj_emit(self.norm_mix(contracted)) + output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) + return mixed * output_gate + + +class TriangleMultiplicativeUpdate(nn.Module): + """Thin wrapper exposing the triangular mixer with explicit orientation (v3).""" + + def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: + super().__init__() + flow = "outgoing" if _outgoing else "incoming" + self._engine = TriangleMultiplicativeBlock( + input_channels=dim, latent_channels=dim, flow=flow + ) + + def set_kernel_backend(self, backend: str | None) -> None: + # Engine uses cueq when backend=="cuequivariance"; the "fused" backend + # routes through the parent PairUpdateBlock's fused path (bypassing this). + self._engine._use_kernels = backend == BACKEND_CUEQ + if backend == BACKEND_CUEQ and not CUE_AVAILABLE: + raise RuntimeError( + "backend='cuequivariance' but cuequivariance_torch is not installed." + ) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._engine.set_chunk_size(chunk_size) + + def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: + return self._engine(z, visibility=mask) + + +# =========================================================================== +# FoldingTrunk: Transition, PairUpdateBlock, FoldingTrunk +# =========================================================================== + + +class Transition(nn.Module): + """LN + SwiGLU FFN with addmm-fused residual; optional Triton LN+w12+SwiGLU kernel.""" + + def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: + super().__init__() + self.norm = nn.LayerNorm(d_model) + self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) + # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. + self._chunk_size: int | None = 64 + self._fused_swiglu: nn.Module | None = None + self._kernel_backend: str | None = None + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def set_kernel_backend(self, backend: str | None) -> None: + """Install / uninstall FusedLNLinearSwiGLU (no cueq equivalent).""" + if backend not in _VALID_BACKENDS: + raise ValueError( + f"backend must be one of {_VALID_BACKENDS}, got {backend!r}" + ) + self._kernel_backend = backend + if backend == BACKEND_FUSED and TRITON_KERNELS_AVAILABLE: + assert _FusedLNLinearSwiGLU is not None + d_model = self.norm.normalized_shape[0] + d_inner = self.ffn.hidden_features + has_ln_bias = self.norm.bias is not None + device = self.ffn.w12.weight.device + dtype = self.ffn.w12.weight.dtype + fused = _FusedLNLinearSwiGLU( + d_model=d_model, + d_inner=d_inner, + has_ln_bias=has_ln_bias, + device=device, + dtype=dtype, + ) + with torch.no_grad(): + fused.LN_W.copy_(self.norm.weight) + if has_ln_bias: + fused.LN_B.copy_(self.norm.bias) # type: ignore[union-attr] + # FusedLNLinearSwiGLU.W12 is (d_model, 2*d_inner); transpose nn.Linear once. + fused.W12.copy_(self.ffn.w12.weight.t().contiguous()) + self._fused_swiglu = fused.eval().requires_grad_(False) + else: + self._fused_swiglu = None + + def _can_use_fused_path(self, x: Tensor) -> bool: + return ( + _fused_active(self, x) + and self._fused_swiglu is not None + and x.dtype == torch.bfloat16 + ) + + def _swiglu_pre_w3(self, x_normed: Tensor) -> Tensor: + """SwiGLU through silu(x1)*x2, before the final w3.""" + ffn = self.ffn + x12 = ffn.w12(x_normed) + x1, x2 = x12.split(ffn.hidden_features, dim=-1) + return F.silu(x1) * x2 + + def _addmm_residual(self, x: Tensor, hidden: Tensor) -> Tensor: + """x + w3(hidden) via single cuBLAS addmm — avoids transition-output allocation.""" + ffn = self.ffn + x_shape = x.shape + out = torch.addmm( + x.contiguous().view(-1, x_shape[-1]), + hidden.view(-1, hidden.shape[-1]), + ffn.w3.weight.t(), + ) + return out.view(x_shape) + + def forward(self, x: Tensor) -> Tensor: + # Inference-only fast path (addmm-fused residual + pre-alloc out) + # — diverges bit-exactly from ``x + ffn(norm(x))`` so we only use + # it when grad is disabled (binder-design / bit-exact tests run + # with grad on and need the reference path). + if not torch.is_grad_enabled() and self._can_use_fused_path(x): + fused = self._fused_swiglu + assert fused is not None + pre_w3 = fused + if self._chunk_size is None or x.shape[1] <= self._chunk_size: + hidden = pre_w3(x) + return self._addmm_residual(x, hidden) + out = torch.empty_like(x) + for s in range(0, x.shape[1], self._chunk_size): + e = min(s + self._chunk_size, x.shape[1]) + sl = x[:, s:e] + hidden = pre_w3(sl) + out[:, s:e] = self._addmm_residual(sl, hidden) + return out + # Reference path — bit-exact with main: x + ffn(norm(x)). + if self._chunk_size is None or x.shape[1] <= self._chunk_size: + return x + self.ffn(self.norm(x)) + out_list: list[Tensor] = [] + for s in range(0, x.shape[1], self._chunk_size): + e = min(s + self._chunk_size, x.shape[1]) + sl = x[:, s:e] + out_list.append(sl + self.ffn(self.norm(sl))) + return torch.cat(out_list, dim=1) + + +class PairUpdateBlock(nn.Module): + """tri_mul_out, tri_mul_in, pair_transition.""" + + def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: + super().__init__() + self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = Transition(d_pair, expansion_ratio=expansion_ratio) + self._kernel_backend: str | None = None + # Row-shared dropout-residual; r=0 for inference (HF model is inference-only). + # backend='fused' swaps in the FusedDropoutResidual Triton kernel. + self.row_drop = DropoutResidual(0.0, batch_dim=1, use_fused_kernels=False) + + def set_kernel_backend(self, backend: str | None) -> None: + if backend not in _VALID_BACKENDS: + raise ValueError( + f"backend must be one of {_VALID_BACKENDS}, got {backend!r}" + ) + self.tri_mul_out.set_kernel_backend(backend) + self.tri_mul_in.set_kernel_backend(backend) + self.pair_transition.set_kernel_backend(backend) + self._kernel_backend = backend + self.row_drop = DropoutResidual( + 0.0, batch_dim=1, use_fused_kernels=(backend == BACKEND_FUSED) + ) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.tri_mul_out.set_chunk_size(chunk_size) + self.tri_mul_in.set_chunk_size(chunk_size) + self.pair_transition.set_chunk_size(chunk_size) + + def _can_use_fused_trimul_with_residual(self, pair: Tensor) -> bool: + return _fused_active(self, pair) and pair.dtype == torch.bfloat16 + + def _fused_trimul_with_residual( + self, pair: Tensor, direction: str, pair_attention_mask: Tensor | None + ) -> Tensor: + """Fused TriMul+residual call; weights from the corresponding engine.""" + tri = self.tri_mul_out if direction == "outgoing" else self.tri_mul_in + engine: TriangleMultiplicativeBlock = tri._engine # type: ignore[assignment] + p_in_weight, g_in_weight = engine.split_kernel_weights() + + def _bf16(t: Tensor) -> Tensor: + return t if t.dtype == torch.bfloat16 else t.to(torch.bfloat16) + + return _fused_trimul_with_residual( # type: ignore[misc] + pair, + direction, + residual=pair, + drop_mask=None, # inference: no dropout, matches internal's eval path + norm_in_weight=_bf16(engine.norm_start.weight), + norm_in_bias=_bf16(engine.norm_start.bias), + p_in_weight=_bf16(p_in_weight), + g_in_weight=_bf16(g_in_weight), + norm_out_weight=_bf16(engine.norm_mix.weight), + norm_out_bias=_bf16(engine.norm_mix.bias), + p_out_weight=_bf16(engine.proj_emit.weight), + g_out_weight=_bf16(engine.proj_gate.weight), + mask=pair_attention_mask, + eps=_EPS, + ) + + def forward( + self, pair: Tensor, pair_attention_mask: Tensor | None = None + ) -> Tensor: + if self._can_use_fused_trimul_with_residual(pair): + pair = self._fused_trimul_with_residual( + pair, "outgoing", pair_attention_mask + ) + pair = self._fused_trimul_with_residual( + pair, "incoming", pair_attention_mask + ) + else: + pair = self.row_drop(pair, self.tri_mul_out(pair, mask=pair_attention_mask)) + pair = self.row_drop(pair, self.tri_mul_in(pair, mask=pair_attention_mask)) + pair = self.pair_transition(pair) + return pair + + +class FoldingTrunk(nn.Module): + """ModuleList of PairUpdateBlocks.""" + + def __init__( + self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4 + ) -> None: + super().__init__() + self.blocks = nn.ModuleList( + [ + PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) + for _ in range(n_layers) + ] + ) + + def set_kernel_backend(self, backend: str | None) -> None: + for block in self.blocks: + cast(PairUpdateBlock, block).set_kernel_backend(backend) + + def set_chunk_size(self, chunk_size: int | None) -> None: + for block in self.blocks: + cast(PairUpdateBlock, block).set_chunk_size(chunk_size) + + def forward( + self, pair: Tensor, pair_attention_mask: Tensor | None = None + ) -> Tensor: + # Cast pair → bf16 internally when the fused trimul backend is enabled + # (its bwd kernel requires bf16). Other backends keep the input dtype. + orig_dtype = pair.dtype + fused_on = ( + len(self.blocks) > 0 + and getattr(self.blocks[0], "_kernel_backend", None) == BACKEND_FUSED + ) + if pair.is_cuda and fused_on and orig_dtype != torch.bfloat16: + pair = pair.to(torch.bfloat16) + for block in self.blocks: + fn = partial(block, pair_attention_mask=pair_attention_mask) + if torch.is_grad_enabled(): + pair = checkpoint(fn, pair, use_reentrant=False) # pyright: ignore + else: + pair = fn(pair) + if pair.dtype != orig_dtype: + pair = pair.to(orig_dtype) + return pair + + +# =========================================================================== +# MSA Encoder +# =========================================================================== + + +class OuterProductMean(nn.Module): + """Outer-product mean: maps an MSA representation into a pair update. + + The order of the ``/ n_valid`` divide vs. the ``Wout`` projection is + selectable via ``divide_outer_before_proj`` because different ESMFold2 + checkpoints were trained with different orderings: + + * ``False`` (default): ``Wout(outer) / n_valid`` — the projection bias + is scaled by 1/n_valid alongside the outer product. + * ``True``: ``Wout(outer / n_valid)`` — the projection bias is added + unscaled, post-divide. + """ + + def __init__( + self, + d_msa: int, + d_hidden: int, + d_pair: int, + divide_outer_before_proj: bool = False, + ) -> None: + super().__init__() + self.d_hidden = d_hidden + self.divide_outer_before_proj = divide_outer_before_proj + self.norm = nn.LayerNorm(d_msa) + self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) + self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) + # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. + self._chunk_size: int | None = None + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: + m_norm = self.norm(m) + x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) + a, b = x.chunk(2, dim=-1) + mask_f = msa_attention_mask.to(a.dtype) + n_valid = (mask_f @ mask_f.transpose(-1, -2)).unsqueeze(-1).clamp(min=1.0) + if self._chunk_size is None: + outer = torch.einsum("bimc,bjmd->bijcd", a, b).flatten(-2) + if self.divide_outer_before_proj: + return self.Wout(outer / n_valid) + return self.Wout(outer) / n_valid + # Chunk along the left (i) axis so the peak einsum intermediate is + # [B, chunk, L, c, d] instead of [B, L, L, c, d]. + L = a.shape[1] + out_chunks: list[Tensor] = [] + for s in range(0, L, self._chunk_size): + e = min(s + self._chunk_size, L) + outer_chunk = torch.einsum("bimc,bjmd->bijcd", a[:, s:e], b).flatten(-2) + if self.divide_outer_before_proj: + out_chunks.append(self.Wout(outer_chunk / n_valid[:, s:e])) + else: + out_chunks.append(self.Wout(outer_chunk) / n_valid[:, s:e]) + return torch.cat(out_chunks, dim=1) + + +class MSAPairWeightedAveraging(nn.Module): + """Pair-biased MSA row update (AF3 Supplement Algorithm 10).""" + + def __init__( + self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32 + ) -> None: + super().__init__() + self.n_heads = n_heads + self.head_width = head_width + self.norm_single = nn.LayerNorm(d_msa) + self.compute_bias = nn.Sequential( + nn.LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False) + ) + self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) + self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) + self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) + + def forward( + self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor + ) -> Tensor: + """ + Args: + msa_repr: [B, L, M, d_msa] + pair_repr: [B, L, L, d_pair] + pair_attention_mask:[B, L, L] + Returns: + [B, L, M, d_msa] + """ + B, L, M, _ = msa_repr.shape + h, dh = self.n_heads, self.head_width + + msa_normed = self.norm_single(msa_repr) + bias = self.compute_bias(pair_repr) # [B, L, L, n_heads] + bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) + attn = torch.softmax(bias, dim=-2) # softmax over j + + v = self.Wv(msa_normed).reshape(B, L, M, h, dh) + gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) + + output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) + return self.Wout(output.reshape(B, L, M, h * dh)) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py new file mode 100644 index 000000000000..255229afbf66 --- /dev/null +++ b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py @@ -0,0 +1,1059 @@ +# coding=utf-8 +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""ESMFold2 experimental variant — older architecture from during the +development of ESMFold2. + +Most users want :class:`ESMFold2Model` (in ``modeling_esmfold2``) instead; +this module retains an explicit refinement loop that re-injects the +previous pair representation through ``pair_loop_proj`` each iteration, +and exists to load checkpoints predating the standard architecture. +""" + +from __future__ import annotations + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor + +from ...modeling_utils import PreTrainedModel # type: ignore[import] +from .configuration_esmfold2 import ESMFold2Config +from .modeling_esmfold2_common import ( + CHAR_VOCAB_SIZE, + MAX_ATOMIC_NUMBER, + NUM_RES_TYPES, + DiffusionStructureHead, + FoldingTrunk, + InputsEmbedder, + LanguageModelShim, + MSAPairWeightedAveraging, + OuterProductMean, + ResIdxAsymIdSymIdEntityIdEncoding, + RowAttentionPooling, + SwiGLUMLP, + TriangleMultiplicativeUpdate, + _categorical_mean, + _compute_intra_token_idx, + _seed_context, + compute_lm_hidden_states, + gather_rep_atom_coords, + gather_token_to_atom, +) + +_EPS = 1e-5 +_NONPOLYMER_ID: int = 3 + + +# =========================================================================== +# ConfidenceHead +# =========================================================================== + + +class ConfidenceHead(nn.Module): + """Confidence head predicting per-atom pLDDT and pairwise PAE.""" + + boundaries: Tensor + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + ch = config.confidence_head + d_single = config.d_single + d_pair = config.d_pair + d_inputs = config.inputs.d_inputs + + # Distogram bins boundary buffer + boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1) + self.register_buffer("boundaries", boundaries) + + self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) + + self.s_norm = nn.LayerNorm(d_single) + + self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) + + self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) + + # s_to_z_prod + self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) + + # s_input_to_s + self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) + + self.s_inputs_norm = nn.LayerNorm(d_inputs) + self.z_norm = nn.LayerNorm(d_pair) + + # Row attention pooling + self.row_attention_pooling = RowAttentionPooling( + d_pair=d_pair, d_single=d_single + ) + + # Confidence folding trunk (4 blocks) + pf = ch.folding_trunk + self.folding_trunk = FoldingTrunk( + n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 + ) + + # pLDDT head + self.plddt_ln = nn.LayerNorm(d_single) + max_atoms_per_token = 23 + self.plddt_weight = nn.Parameter( + torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins) + ) + + # PAE head + self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) + + # ------------------------------------------------------------------ + # Kernel / chunking configuration + # ------------------------------------------------------------------ + + def set_kernel_backend(self, backend: str | None) -> None: + self.folding_trunk.set_kernel_backend(backend) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.folding_trunk.set_chunk_size(chunk_size) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor: + if num_diffusion_samples == 1: + return x + return x.repeat_interleave(num_diffusion_samples, 0) + + @staticmethod + def _flatten_sample_axis(x: Tensor) -> Tensor: + if x.ndim == 4: + b, mult, n, c = x.shape + return x.reshape(b * mult, n, c) + return x + + # ------------------------------------------------------------------ + # Forward + # ------------------------------------------------------------------ + + def forward( + self, + s_inputs: Tensor, + z: Tensor, + x_pred: Tensor, + distogram_atom_idx: Tensor, + token_attention_mask: Tensor, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + asym_id: Tensor, + mol_type: Tensor, + num_diffusion_samples: int = 1, + relative_position_encoding: Tensor | None = None, + token_bonds_encoding: Tensor | None = None, + ) -> dict[str, Tensor]: + """Run confidence head.""" + # Shared computation (batch-scale, before num_diffusion_samples expansion) + s_inputs_normed = self.s_inputs_norm(s_inputs) + + z_base = self.z_norm(z) + if relative_position_encoding is not None: + z_base = z_base + relative_position_encoding + if token_bonds_encoding is not None: + z_base = z_base + token_bonds_encoding + z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2) + z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1) + z_base = z_base + self.s_to_z_prod_out( + self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] + * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] + ) + + # Expand to num_diffusion_samples + pair = self._repeat_batch(z_base, num_diffusion_samples) + x_pred_flat = self._flatten_sample_axis(x_pred) + atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples) + atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples) + rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long() + mask = self._repeat_batch(token_attention_mask, num_diffusion_samples) + Bm = pair.shape[0] + + # Distogram from predicted coords + rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) + rep_distances = torch.cdist( + rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist" + ) + distogram_bins = ( + (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() + ) + pair = pair + self.dist_bin_pairwise_embed(distogram_bins) + + # Expand 1-D token mask → 2-D pair mask for folding trunk + pair_mask = mask[:, :, None].float() * mask[:, None, :].float() # [B*m, L, L] + + # FoldingTrunk + row attention pooling -> single + pair = pair + self.folding_trunk(pair, pair_attention_mask=pair_mask) + single = self.row_attention_pooling(pair, mask) + + # Per-atom pLDDT + atom_mask_f = atom_mask_m.float() + s_at_atoms = gather_token_to_atom(single, atom_to_token_m) + s_at_atoms = self.plddt_ln(s_at_atoms) + + intra_idx = _compute_intra_token_idx(atom_to_token_m) + intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) + w = self.plddt_weight[intra_idx] # [B*m, A, d_single, num_bins] + plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms, w) + + plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0) + + # Per-token pLDDT (scatter mean) + L = single.shape[1] + plddt_sum = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) + atom_count = torch.zeros( + Bm, L, device=single.device, dtype=plddt_per_atom.dtype + ) + atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype) + plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t) + atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) + plddt = plddt_sum / atom_count.clamp(min=1e-6) + + # Complex pLDDT (flat mean over all atoms) + complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / ( + atom_mask_f.sum(dim=-1) + _EPS + ) + + # Complex ipLDDT (interface-weighted) + expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) + expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) + is_ligand = (expanded_type == _NONPOLYMER_ID).float() + inter_chain = ( + expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) + ).float() + near_contact = (rep_distances < 8).float() + interface_per_token = ( + near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1) + ).amax(dim=-1) + iplddt_weight = torch.where( + is_ligand.bool(), + torch.full_like(interface_per_token, 2.0), + interface_per_token, + ) + iplddt_weight_atoms = gather_token_to_atom( + iplddt_weight.unsqueeze(-1), atom_to_token_m + ).squeeze(-1) + atom_iplddt_w = atom_mask_f * iplddt_weight_atoms + complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / ( + atom_iplddt_w.sum(dim=-1) + _EPS + ) + + # pLDDT at CA / representative atom + plddt_ca = plddt_per_atom.gather(1, rep_idx_m) + + # PAE + pae_logits = self.pae_head(pair) + pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() + + # pTM / ipTM / per-chain-pair ipTM derived from pae_logits. + n_bins = pae_logits.shape[-1] + bin_width = 32.0 / n_bins + bin_centers = torch.arange( + 0.5 * bin_width, 32.0, bin_width, device=pae_logits.device + ) + mask_f = mask.float() + N_res = mask_f.sum(dim=-1, keepdim=True) + d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 # [Bm, 1] + tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2) # [Bm, n_bins] + pae_probs = F.softmax(pae_logits, dim=-1) + tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum( + dim=-1 + ) # [Bm, L, L] + + pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) # [Bm, L, L] + + # pTM: avg over all valid pairs per row, max over rows. + ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / ( + pair_mask_2d.sum(dim=-1) + _EPS + ) + ptm = ptm_per_row.max(dim=-1).values # [Bm] + + # ipTM: avg over inter-chain valid pairs per row, max over rows. + inter_chain_mask = ( + expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) + ).float() * pair_mask_2d + iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / ( + inter_chain_mask.sum(dim=-1) + _EPS + ) + iptm = iptm_per_row.max(dim=-1).values # [Bm] + + # Per-chain-pair ipTM: dense [Bm, N_chains, N_chains] padded to max chain id + 1. + max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 + n_chains = max_chain_id + 1 + pair_chains_iptm = torch.zeros( + Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype + ) + for c1 in range(n_chains): + chain_c1 = (expanded_asym == c1).float() * mask_f + if chain_c1.sum() == 0: + continue + for c2 in range(n_chains): + chain_c2 = (expanded_asym == c2).float() * mask_f + pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) + denom = pair_m.sum(dim=(-1, -2)) + _EPS + pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum( + dim=(-1, -2) + ) / denom + + return { + "plddt_logits": plddt_logits, + "plddt": plddt.detach(), + "plddt_per_atom": plddt_per_atom.detach(), + "plddt_ca": plddt_ca.detach(), + "complex_plddt": complex_plddt.detach(), + "complex_iplddt": complex_iplddt.detach(), + "pae_logits": pae_logits, + "pae": pae, + "ptm": ptm.detach(), + "iptm": iptm.detach(), + "pair_chains_iptm": pair_chains_iptm.detach(), + } + + +# =========================================================================== +# MSA Encoder +# =========================================================================== + + +class _TransitionFFN(nn.Module): + """LayerNorm + SwiGLU FFN without residual (used inside MSAEncoderBlock).""" + + def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: + super().__init__() + self.norm = nn.LayerNorm(d_model) + self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) + + def forward(self, x: Tensor) -> Tensor: + return self.ffn(self.norm(x)) + + +class MSAEncoderBlock(nn.Module): + """One block of the MSA encoder: MSA update + pair update.""" + + def __init__( + self, + d_msa: int, + d_pair: int, + d_hidden: int = 32, + n_heads_msa: int = 8, + msa_head_width: int = 32, + ) -> None: + super().__init__() + self.outer_product_mean = OuterProductMean( + d_msa, d_hidden, d_pair, divide_outer_before_proj=True + ) + self.msa_pair_weighted_averaging = MSAPairWeightedAveraging( + d_msa, d_pair, n_heads_msa, msa_head_width + ) + self.msa_transition = _TransitionFFN(d_msa, expansion_ratio=4) + self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = _TransitionFFN(d_pair, expansion_ratio=4) + + def forward( + self, + msa_repr: Tensor, + pair_repr: Tensor, + msa_attention_mask: Tensor, + pair_attention_mask: Tensor, + msa_track_mask: Tensor | None = None, + ) -> tuple[Tensor, Tensor]: + """ + Args: + msa_repr: [B, L, M, d_msa] + pair_repr: [B, L, L, d_pair] + msa_attention_mask: [B, L, M] + pair_attention_mask:[B, L, L] + msa_track_mask: [B] bool — if False for a sample, zero out its contribution + Returns: + (msa_repr, pair_repr) + """ + mask4d = ( + msa_track_mask[:, None, None, None].to(dtype=msa_repr.dtype) + if msa_track_mask is not None + else None + ) + + def _maybe_mask(x: Tensor) -> Tensor: + return x * mask4d if mask4d is not None else x + + msa_repr = msa_repr + _maybe_mask( + self.msa_pair_weighted_averaging(msa_repr, pair_repr, pair_attention_mask) + ) + msa_repr = msa_repr + _maybe_mask(self.msa_transition(msa_repr)) + + pair_repr = pair_repr + _maybe_mask( + self.outer_product_mean(msa_repr, msa_attention_mask) + ) + pair_repr = pair_repr + _maybe_mask( + self.tri_mul_out(pair_repr, mask=pair_attention_mask) + ) + pair_repr = pair_repr + _maybe_mask( + self.tri_mul_in(pair_repr, mask=pair_attention_mask) + ) + pair_repr = pair_repr + _maybe_mask(self.pair_transition(pair_repr)) + + return msa_repr, pair_repr + + +class MSAEncoder(nn.Module): + """Embeds MSA features and runs encoder blocks to update the pair representation.""" + + def __init__( + self, + d_msa: int, + d_pair: int, + d_inputs: int, + d_hidden: int = 32, + n_layers: int = 4, + n_heads_msa: int = 8, + msa_head_width: int = 32, + ) -> None: + super().__init__() + # 33 aa one-hot + has_deletion + deletion_value = 35 + self.embed = nn.Linear(35, d_msa, bias=False) + self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) + self.blocks = nn.ModuleList( + [ + MSAEncoderBlock( + d_msa=d_msa, + d_pair=d_pair, + d_hidden=d_hidden, + n_heads_msa=n_heads_msa, + msa_head_width=msa_head_width, + ) + for _ in range(n_layers) + ] + ) + + def forward( + self, + x_pair: Tensor, + x_inputs: Tensor, + msa_oh: Tensor, + has_deletion: Tensor, + deletion_value: Tensor, + msa_attention_mask: Tensor, + ) -> Tensor: + """ + Args: + x_pair: [B, L, L, d_pair] current pair representation + x_inputs: [B, L, d_inputs] per-token input features + msa_oh: [B, L, M, 33] one-hot MSA (already transposed) + has_deletion: [B, L, M] + deletion_value: [B, L, M] + msa_attention_mask:[B, L, M] + Returns: + [B, L, L, d_pair] pair update + """ + B, L, M = msa_attention_mask.shape + + m_feat = torch.cat( + [msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1 + ) + m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2) + + # Mask out the full update for samples with no real non-query MSA rows. + if M > 1: + msa_track_mask = msa_attention_mask[:, :, 1:].any(dim=(1, 2)) + else: + msa_track_mask = torch.zeros(B, dtype=torch.bool, device=x_pair.device) + + tok_mask = msa_attention_mask[:, :, 0] + pair_attention_mask = tok_mask.unsqueeze(2) * tok_mask.unsqueeze(1) + + for block in self.blocks: + m, x_pair = block( + m, x_pair, msa_attention_mask, pair_attention_mask, msa_track_mask + ) + + x_pair = x_pair * msa_track_mask[:, None, None, None].to(dtype=x_pair.dtype) + return x_pair + + +# =========================================================================== +# ESMFold2ExperimentalModel — the top-level PreTrainedModel +# =========================================================================== + + +class ESMFold2ExperimentalModel(PreTrainedModel): + """ESMFold2 v2 structure prediction model.""" + + config_class = ESMFold2Config + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__(config) + + # InputsEmbedder + self.inputs_embedder = InputsEmbedder(config) + + # z_init projections + d_inputs = config.inputs.d_inputs + d_pair = config.d_pair + + self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) + self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False) + + # Trunk relative position encoding + self.rel_pos = ResIdxAsymIdSymIdEntityIdEncoding( + n_relative_residx_bins=config.n_relative_residx_bins, + n_relative_chain_bins=config.n_relative_chain_bins, + d_pair=d_pair, + ) + + # Token bonds + self.token_bonds = nn.Linear(1, d_pair, bias=False) + + self.language_model = LanguageModelShim( + d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers + ) + self._esmc: nn.Module | None = None # lazily loaded + + # FoldingTrunk + pf = config.folding_trunk + self.folding_trunk = FoldingTrunk( + n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 + ) + + # Per-loop pair re-injection projection + self.pair_loop_proj = nn.Sequential( + nn.LayerNorm(d_pair), nn.Linear(d_pair, d_pair, bias=False) + ) + nn.init.zeros_(self.pair_loop_proj[1].weight) # type: ignore[arg-type] + + # Structure head + self.structure_head = DiffusionStructureHead(config) + + # Distogram head + self.distogram_head = nn.Linear( + d_pair, config.structure_head.distogram_bins, bias=True + ) + + if config.confidence_head.enabled: + self.confidence_head: ConfidenceHead | None = ConfidenceHead(config) + else: + self.confidence_head = None + + # MSA encoder (Large MSA models only) + msa_cfg = config.msa_encoder + if msa_cfg.enabled: + self.msa_encoder: MSAEncoder | None = MSAEncoder( + d_msa=msa_cfg.d_msa, + d_pair=d_pair, + d_inputs=d_inputs, + d_hidden=msa_cfg.d_hidden, + n_layers=msa_cfg.n_layers, + n_heads_msa=msa_cfg.n_heads_msa, + msa_head_width=msa_cfg.msa_head_width, + ) + else: + self.msa_encoder = None + + self.post_init() + + def set_kernel_backend(self, backend: str | None) -> None: + """Select kernel backend (None / "fused" / "cuequivariance").""" + self.folding_trunk.set_kernel_backend(backend) + if self.confidence_head is not None: + self.confidence_head.set_kernel_backend(backend) + self.structure_head.set_kernel_backend(backend) + + def set_chunk_size(self, chunk_size: int | None) -> None: + """Set chunk size for memory-efficient triangle multiplicative updates.""" + self.folding_trunk.set_chunk_size(chunk_size) + if self.confidence_head is not None: + self.confidence_head.set_chunk_size(chunk_size) + + def configure_lm_dropout( + self, lm_dropout: float, *, force_lm_dropout_during_inference: bool = True + ) -> None: + """Configure LM embedding dropout (binder-design / critic scoring).""" + self.config.lm_dropout = lm_dropout + self.config.force_lm_dropout_during_inference = ( + force_lm_dropout_during_inference + ) + + def load_esmc(self, esmc_model_path: str) -> None: + """Load the ESMC LM backbone from a HuggingFace Hub repo ID or local directory.""" + from ..esmc.modeling_esmc import ESMCModel # type: ignore[import] + + esmc = ESMCModel.from_pretrained(esmc_model_path) + self._esmc = esmc.bfloat16().to(self.device).eval() + + @classmethod + def from_pretrained( + cls, pretrained_model_name_or_path, *args, load_esmc: bool = True, **kwargs + ): + model = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs) + if load_esmc: + model.load_esmc(model.config.esmc_id) + return model + + @torch.no_grad() + def infer_protein(self, seq: str, **forward_kwargs) -> dict: + from .protein_utils import OUTPUT_TO_PDB_FEATURE_KEYS, prepare_protein_features + + features = prepare_protein_features(seq) + features = {k: v.to(self.device) for k, v in features.items()} + output = self(**features, **forward_kwargs) + for k in OUTPUT_TO_PDB_FEATURE_KEYS: + output[k] = features[k] + return output + + def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str: + return self.output_to_pdb(self.infer_protein(seq, **forward_kwargs)) + + @torch.no_grad() + def infer_all_atom(self, structure_input, **forward_kwargs): + try: + import evolutionaryscale.opensource as esm # TODO: change to `import esm` when open sourcing + import evolutionaryscale.opensource.models.esmfold2 # noqa: F401 # TODO: drop when open sourcing + except ImportError as e: + raise NotImplementedError( + "All-atom inference requires the `esm` companion package: " + "`pip install esm`." + ) from e + esmfold2 = esm.models.esmfold2 # type: ignore[attr-defined] + + if isinstance(structure_input, esmfold2.ProteinInput): + structure_input = esmfold2.StructurePredictionInput( + sequences=[structure_input] + ) + processor = esmfold2.ESMFold2InputBuilder() + features, chain_infos = processor.prepare_input(structure_input) + features = { + k: v.to(self.device) if isinstance(v, Tensor) else v + for k, v in features.items() + } + output = self(**features, **forward_kwargs) + return self._output_to_molecular_complex(output, features, chain_infos) + + @staticmethod + def _output_to_molecular_complex(output: dict, features: dict, chain_infos: list): + import evolutionaryscale.opensource as esm # TODO: change to `import esm` when open sourcing + import evolutionaryscale.opensource.models.esmfold2 # noqa: F401 # TODO: drop when open sourcing + + esmfold2 = esm.models.esmfold2 # type: ignore[attr-defined] + + ELEMENT_NUMBER_TO_SYMBOL = esmfold2.ELEMENT_NUMBER_TO_SYMBOL + MolecularComplex = esmfold2.MolecularComplex + MolecularComplexMetadata = esmfold2.MolecularComplexMetadata + + coords = output["sample_atom_coords"] + if coords.dim() == 4: + coords = coords[:, 0] + coords_np = coords.detach().cpu().numpy() + + plddt = output["plddt"].detach().cpu().numpy() + atom_to_token = features["atom_to_token"].cpu().numpy() + ref_chars = features["ref_atom_name_chars"].cpu().numpy() + ref_element = features["ref_element"].cpu().numpy() + atom_mask = features["atom_attention_mask"].cpu().numpy().astype(bool) + + if atom_to_token.ndim == 1: + atom_to_token = atom_to_token[None] + ref_chars = ref_chars[None] + ref_element = ref_element[None] + atom_mask = atom_mask[None] + + b = 0 + atoms_by_token: dict[int, list[int]] = {} + for a in range(atom_to_token.shape[1]): + if not atom_mask[b, a]: + continue + atoms_by_token.setdefault(int(atom_to_token[b, a]), []).append(a) + + flat_positions: list[np.ndarray] = [] + flat_elements: list[str] = [] + flat_names: list[str] = [] + flat_hetero: list[bool] = [] + sequence_tokens: list[str] = [] + token_to_atoms: list[list[int]] = [] + chain_ids_per_token: list[int] = [] + confidence_scores: list[float] = [] + chain_lookup: dict[int, str] = {} + entity_info: dict[int, str] = {} + + cursor = 0 + for chain in chain_infos: + chain_lookup[chain.asym_id] = chain.chain_id + entity_info[chain.entity_id] = ( + "polymer" if chain.mol_type != 3 else "non-polymer" + ) + for tok in chain.tokens: + sequence_tokens.append(tok.residue_name) + chain_ids_per_token.append(chain.asym_id) + confidence_scores.append(float(plddt[b, tok.token_index])) + start = cursor + for a in atoms_by_token.get(tok.token_index, []): + flat_positions.append(coords_np[b, a]) + name = "".join( + chr(int(c) + 32) if int(c) != 0 else " " + for c in ref_chars[b, a] + ).strip() + flat_names.append(name) + flat_elements.append( + ELEMENT_NUMBER_TO_SYMBOL.get(int(ref_element[b, a]), "X") + ) + flat_hetero.append(chain.mol_type == 3) + cursor += 1 + token_to_atoms.append([start, cursor]) + + return MolecularComplex( + id="prediction", + sequence=sequence_tokens, + atom_positions=np.array(flat_positions, dtype=np.float32), + atom_elements=np.array(flat_elements, dtype=object), + token_to_atoms=np.array(token_to_atoms, dtype=np.int32), + chain_id=np.array(chain_ids_per_token, dtype=np.int64), + plddt=np.array(confidence_scores, dtype=np.float32), + atom_names=np.array(flat_names, dtype=object), + atom_hetero=np.array(flat_hetero, dtype=bool), + metadata=MolecularComplexMetadata( + entity_lookup={k: str(v) for k, v in entity_info.items()}, + chain_lookup=chain_lookup, + assembly_composition=None, + ), + ) + + @staticmethod + def output_to_pdb(output: dict) -> str: + from .protein_utils import output_to_pdb as _output_to_pdb + + return _output_to_pdb(output) + + def _compute_lm_hidden_states( + self, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + token_mask: Tensor, + ) -> Tensor: + """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers.""" + assert self._esmc is not None + return compute_lm_hidden_states( + self._esmc, input_ids, asym_id, residue_index, mol_type, token_mask + ) + + def forward( + self, + # Token features + token_index: Tensor, + residue_index: Tensor, + asym_id: Tensor, + sym_id: Tensor, + entity_id: Tensor, + mol_type: Tensor, + res_type: Tensor, + token_bonds: Tensor, + token_attention_mask: Tensor, + # Atom features + ref_pos: Tensor, + ref_element: Tensor, + ref_charge: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + atom_attention_mask: Tensor, + atom_to_token: Tensor, + distogram_atom_idx: Tensor, + # MSA features + deletion_mean: Tensor | None = None, + msa: Tensor | None = None, + has_deletion: Tensor | None = None, + deletion_value: Tensor | None = None, + msa_attention_mask: Tensor | None = None, + # LM features (auto-computed from input_ids if ESMC loaded) + input_ids: Tensor | None = None, + lm_hidden_states: Tensor | None = None, + # Used in design to provide a soft sequence input. + res_type_soft: Tensor | None = None, + # Inference config + num_loops: int | None = None, + num_diffusion_samples: int | None = None, + num_sampling_steps: int | None = None, + early_exit: bool = False, + seed: int | None = None, + **kwargs, + ) -> dict[str, Tensor]: + """Full ESMFold2 inference pipeline. + + Accepts tensors directly from ESMFold2InputBuilder.prepare_input(). + + Returns: + dict with sample_atom_coords, plddt, pae, distogram_logits, etc. + """ + tok_mask = token_attention_mask + atm_mask = atom_attention_mask + disto_idx = distogram_atom_idx + + n_loops: int = num_loops if num_loops is not None else self.config.num_loops + n_samples: int = ( + num_diffusion_samples + if num_diffusion_samples is not None + else self.config.num_diffusion_samples + ) + + # One-hot res_type for input embedder concatenation + if res_type.dim() == 2: + res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float() + res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() + else: + res_type_oh = res_type.float() + + # Profile: masked mean over MSA depth, with res_type fallback when no MSA. + if msa is not None: + msa_oh_profile = F.one_hot( + msa.long(), num_classes=NUM_RES_TYPES + ).float() # [B, M, L, V] + if msa_attention_mask is not None: + mask_f = msa_attention_mask.float().unsqueeze(-1) # [B, M, L, 1] + msa_oh_profile = msa_oh_profile * mask_f + valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) + profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) + else: + profile = msa_oh_profile.mean(dim=1) + else: + profile = res_type_oh + + # Used in design to provide a soft sequence input. + if res_type_soft is not None: + res_type_oh = res_type_soft.float() + if not getattr(self.config, "disable_msa_features", False) and kwargs.get( + "provide_soft_sequence_to_msa_and_profile", True + ): + profile = res_type_oh + msa = res_type_oh.unsqueeze(1) + msa_attention_mask = tok_mask.unsqueeze(1) + + if deletion_mean is None: + deletion_mean = torch.zeros( + res_type.shape[0], res_type.shape[1], device=res_type.device + ) + + if getattr(self.config, "disable_msa_features", False): + profile = torch.zeros_like(profile) + deletion_mean = torch.zeros_like(deletion_mean) + + ref_element = F.one_hot( + ref_element.long(), num_classes=MAX_ATOMIC_NUMBER + ).float() + ref_atom_name_chars = F.one_hot( + ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE + ).float() + # Bias-free downstream Linears require zeroed padding. + atm_mask_f = atm_mask.float() + ref_element = ref_element * atm_mask_f.unsqueeze(-1) + ref_atom_name_chars = ref_atom_name_chars * atm_mask_f.unsqueeze(-1).unsqueeze( + -1 + ) + + atom_to_token = atom_to_token * atm_mask.long() + + use_amp = ref_pos.device.type == "cuda" + with ( + torch.set_grad_enabled(res_type_soft is not None), + torch.amp.autocast("cuda", enabled=use_amp, dtype=torch.bfloat16), + ): + # 1. Input embeddings + x_inputs = self.inputs_embedder( + aatype=res_type_oh, + profile=profile.float(), + deletion_mean=deletion_mean.float(), + ref_pos=ref_pos, + atom_attention_mask=atm_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=atom_to_token, + ) + + # 2. Initialize pair representation + z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2( + x_inputs + ).unsqueeze(1) + + # 3. Positional encodings + relative_position_encoding = self.rel_pos( + residue_index=residue_index, + asym_id=asym_id, + sym_id=sym_id, + entity_id=entity_id, + token_index=token_index, + ) + token_bonds_encoding = self.token_bonds(token_bonds.float()) + z_init = z_init + relative_position_encoding + token_bonds_encoding + + # 4. Language model integration + if ( + lm_hidden_states is None + and input_ids is not None + and self._esmc is not None + ): + lm_hidden_states = self._compute_lm_hidden_states( + input_ids, asym_id, residue_index, mol_type, tok_mask + ) + if lm_hidden_states is not None: + lm_z = self.language_model( + lm_hidden_states.detach(), lm_dropout=self.config.lm_dropout + ) + z_init = z_init + lm_z.to(z_init.dtype) + + # MSA tensors prepared once: encoder consumes [B, L, M, ...] layout. + _msa_kwargs: dict | None = None + if self.msa_encoder is not None and msa is not None: + if msa.dim() == 4: + B_msa, M, L_msa, _ = msa.shape + msa_oh = msa.permute(0, 2, 1, 3).float() + else: + B_msa, M, L_msa = msa.shape + msa_oh = F.one_hot( + msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES + ).float() # [B, L, M, 33] + msa_attn = ( + msa_attention_mask.permute(0, 2, 1).float() + if msa_attention_mask is not None + else tok_mask[:, :, None].expand(-1, -1, M).float() + ) + # Bias-free MSAEncoder.embed requires zeroed padding. + msa_oh = msa_oh * msa_attn.unsqueeze(-1) + hd = ( + has_deletion.permute(0, 2, 1).float() + if has_deletion is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + dv = ( + deletion_value.permute(0, 2, 1).float() + if deletion_value is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + _msa_kwargs = dict( + x_inputs=x_inputs, + msa_oh=msa_oh, + has_deletion=hd, + deletion_value=dv, + msa_attention_mask=msa_attn, + ) + + # Expand 1-D token mask → 2-D pair mask for folding trunk + pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() + + # Loop: pair-only folding trunk, MSA encoder runs inside each iteration. + z = torch.zeros_like(z_init) + prev_pair: Tensor | None = None + prev_disto_probs: Tensor | None = None + for loop_num in range(n_loops + 1): + z = z_init + self.pair_loop_proj(z) + if _msa_kwargs is not None and self.msa_encoder is not None: + z = z + self.msa_encoder(x_pair=z, **_msa_kwargs).to(z.dtype) + z = self.folding_trunk(z, pair_attention_mask=pair_mask) + + # Loop early-exit + if early_exit and loop_num < n_loops: + l2_converged = False + if prev_pair is not None and loop_num > 0: + rel_l2 = ( + z.float() - prev_pair.float() + ).norm() / prev_pair.float().norm().clamp(min=1e-8) + l2_converged = rel_l2.item() < 0.25 + prev_pair = z.detach().clone() + + sym_z = z.float() + z.float().transpose(-2, -3) + cur_probs = F.softmax(self.distogram_head(sym_z).float(), dim=-1) + if prev_disto_probs is not None and loop_num > 0: + kl_per_pair = ( + cur_probs + * ( + cur_probs.clamp(min=1e-8) + / prev_disto_probs.clamp(min=1e-8) + ).log() + ).sum(-1) + kl = (kl_per_pair + kl_per_pair.transpose(-1, -2)).mean() / 2 + if l2_converged or kl.item() < 0.05: + break + prev_disto_probs = cur_probs.detach() + + # 6. Distogram (inside the trunk autocast so z stays bf16) + distogram_logits = self.distogram_head(z + z.transpose(-2, -3)) + + # 7. Diffusion sampling (always no_grad; optional seed for parity) + with torch.no_grad(), _seed_context(seed): + structure_output = self.structure_head.sample( + z_trunk=z.float(), + s_inputs=x_inputs, + s_trunk=None, + relative_position_encoding=relative_position_encoding, + ref_pos=ref_pos, + ref_charge=ref_charge, + ref_mask=atm_mask, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + ref_space_uid=ref_space_uid, + tok_idx=atom_to_token, + asym_id=asym_id, + residue_index=residue_index, + entity_id=entity_id, + token_index=token_index, + sym_id=sym_id, + token_attention_mask=tok_mask, + num_diffusion_samples=n_samples, + num_sampling_steps=num_sampling_steps, + return_atom_repr=False, + denoising_early_exit_rmsd=(0.10 if early_exit else None), + ) + + sample_coords = structure_output["sample_atom_coords"] + assert sample_coords is not None + output: dict[str, Tensor] = {"distogram_logits": distogram_logits} + output["sample_atom_coords"] = sample_coords + + if self.confidence_head is not None: + confidence_output = self.confidence_head( + s_inputs=x_inputs.detach(), + z=z.detach().float(), + x_pred=sample_coords.detach(), + distogram_atom_idx=disto_idx, + token_attention_mask=tok_mask, + atom_to_token=atom_to_token, + atom_attention_mask=atm_mask, + asym_id=asym_id, + mol_type=mol_type, + num_diffusion_samples=n_samples, + relative_position_encoding=relative_position_encoding.detach(), + token_bonds_encoding=token_bonds_encoding.detach(), + ) + output.update(confidence_output) + + # Pass-through tensors used by output decoders. + output["atom_pad_mask"] = ( + atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask + ) + output["residue_index"] = residue_index + output["entity_id"] = entity_id + + return output + + +__all__ = ["ESMFold2ExperimentalModel"] diff --git a/src/transformers/models/esmfold2/protein_utils.py b/src/transformers/models/esmfold2/protein_utils.py new file mode 100644 index 000000000000..a94c05c9c575 --- /dev/null +++ b/src/transformers/models/esmfold2/protein_utils.py @@ -0,0 +1,582 @@ +# coding=utf-8 +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Self-contained protein featurization for ESMFold2 inference.""" + +from __future__ import annotations + +import math + +import numpy as np +import torch +from torch import Tensor + +MOL_TYPE_PROTEIN = 0 +PROTEIN_UNK_RES_TYPE = 22 +MSA_GAP_TOKEN_ID = 1 + +PROTEIN_RESIDUE_TO_RES_TYPE: dict[str, int] = { + "ALA": 2, + "ARG": 3, + "ASN": 4, + "ASP": 5, + "CYS": 6, + "GLN": 7, + "GLU": 8, + "GLY": 9, + "HIS": 10, + "ILE": 11, + "LEU": 12, + "LYS": 13, + "MET": 14, + "PHE": 15, + "PRO": 16, + "SER": 17, + "THR": 18, + "TRP": 19, + "TYR": 20, + "VAL": 21, +} + +PROTEIN_1TO3: dict[str, str] = { + "A": "ALA", + "R": "ARG", + "N": "ASN", + "D": "ASP", + "C": "CYS", + "Q": "GLN", + "E": "GLU", + "G": "GLY", + "H": "HIS", + "I": "ILE", + "L": "LEU", + "K": "LYS", + "M": "MET", + "F": "PHE", + "P": "PRO", + "S": "SER", + "T": "THR", + "W": "TRP", + "Y": "TYR", + "V": "VAL", + "X": "UNK", +} + +ESM_PROTEIN_VOCAB: dict[str, int] = { + "L": 4, + "A": 5, + "G": 6, + "V": 7, + "S": 8, + "E": 9, + "R": 10, + "T": 11, + "I": 12, + "D": 13, + "P": 14, + "K": 15, + "Q": 16, + "N": 17, + "F": 18, + "Y": 19, + "M": 20, + "H": 21, + "W": 22, + "C": 23, + "X": 3, +} + +# Heavy atoms per canonical residue, in training-time order. +PROTEIN_HEAVY_ATOMS: dict[str, list[str]] = { + "ALA": ["N", "CA", "C", "O", "CB"], + "ARG": ["N", "CA", "C", "O", "CB", "CG", "CD", "NE", "CZ", "NH1", "NH2"], + "ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2"], + "ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2"], + "CYS": ["N", "CA", "C", "O", "CB", "SG"], + "GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2"], + "GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2"], + "GLY": ["N", "CA", "C", "O"], + "HIS": ["N", "CA", "C", "O", "CB", "CG", "ND1", "CD2", "CE1", "NE2"], + "ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1"], + "LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2"], + "LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ"], + "MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"], + "PHE": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ"], + "PRO": ["N", "CA", "C", "O", "CB", "CG", "CD"], + "SER": ["N", "CA", "C", "O", "CB", "OG"], + "THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2"], + "TRP": [ + "N", + "CA", + "C", + "O", + "CB", + "CG", + "CD1", + "CD2", + "NE1", + "CE2", + "CE3", + "CZ2", + "CZ3", + "CH2", + ], + "TYR": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "OH"], + "VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2"], + "UNK": ["N", "CA", "C", "O"], +} + +PROTEIN_REF_POS: dict[str, dict[str, tuple[float, float, float]]] = { + "ALA": { + "N": (-0.01003183238208294, -1.2073018550872803, -1.0555061101913452), + "CA": (-0.04190138354897499, 0.17447763681411743, -0.5729365348815918), + "C": (1.2127548456192017, 0.4737588167190552, 0.19521640241146088), + "O": (1.9390329122543335, 1.4484562873840332, -0.13759790360927582), + "CB": (-1.276943325996399, 0.4288230538368225, 0.29937705397605896), + }, + "ARG": { + "N": (-2.0170421600341797, 0.6717798113822937, -1.1794233322143555), + "CA": (-2.0503084659576416, -0.5735036730766296, -0.4097220301628113), + "C": (-3.469440460205078, -1.0612813234329224, -0.2755832374095917), + "O": (-3.8218462467193604, -2.1369943618774414, -0.8294969797134399), + "CB": (-1.4193516969680786, -0.3735991418361664, 0.9852858781814575), + "CG": (0.11878877878189087, -0.3112654983997345, 0.963895857334137), + "CD": (0.6643245816230774, 1.0068185329437256, 0.3963329493999481), + "NE": (2.1090238094329834, 1.0977025032043457, 0.6120952367782593), + "CZ": (3.098905324935913, 0.3215920031070709, -0.09047172218561172), + "NH1": (4.461230278015137, 0.3844667971134186, 0.34141138195991516), + "NH2": (2.7856509685516357, -0.4166366159915924, -1.1148239374160767), + }, + "ASN": { + "N": (-0.7595629096031189, 0.7503494620323181, 1.1369825601577759), + "CA": (-0.76087886095047, 0.23876343667507172, -0.23573364317417145), + "C": (-1.9211044311523438, -0.6982439160346985, -0.42196929454803467), + "O": (-2.677666187286377, -0.5753439664840698, -1.4223182201385498), + "CB": (0.5504899024963379, -0.5078350305557251, -0.5390339493751526), + "CG": (1.7250099182128906, 0.4264017939567566, -0.5778228640556335), + "OD1": (1.9470350742340088, 1.1086392402648926, -1.613560438156128), + "ND2": (2.57365345954895, 0.5730618834495544, 0.5608599781990051), + }, + "ASP": { + "N": (-1.8452696800231934, -1.2169504165649414, 0.19437327980995178), + "CA": (-0.6379959583282471, -0.41974392533302307, 0.41681644320487976), + "C": (-0.9431572556495667, 1.0356197357177734, 0.18555717170238495), + "O": (-1.5183608531951904, 1.4045922756195068, -0.8739855885505676), + "CB": (0.48594576120376587, -0.8970447778701782, -0.5209363698959351), + "CG": (1.780342936515808, -0.19918935000896454, -0.2310730367898941), + "OD1": (2.5202910900115967, -0.6044584512710571, 0.7049641013145447), + "OD2": (2.1454880237579346, 0.9208861589431763, -0.9712985157966614), + }, + "CYS": { + "N": (0.0469963513314724, 1.190075159072876, -1.1607273817062378), + "CA": (0.11344368755817413, -0.09400428831577301, -0.45952197909355164), + "C": (-1.2652032375335693, -0.6832379698753357, -0.3594406247138977), + "O": (-1.4631439447402954, -1.8851220607757568, -0.6826791763305664), + "CB": (0.6919880509376526, 0.09034398198127747, 0.952482283115387), + "SG": (2.4619927406311035, 0.5235707759857178, 0.9020372629165649), + }, + "GLN": { + "N": (-2.370004653930664, -0.9637529850006104, -0.7942749261856079), + "CA": (-1.370002269744873, -0.6000258922576904, 0.2103111445903778), + "C": (-1.7545503377914429, 0.7091967463493347, 0.8433493971824646), + "O": (-1.8520662784576416, 0.7999289631843567, 2.0964975357055664), + "CB": (0.02040259726345539, -0.5004461407661438, -0.44764479994773865), + "CG": (1.1377512216567993, -0.28680720925331116, 0.582992434501648), + "CD": (2.4745187759399414, -0.24800164997577667, -0.09364881366491318), + "OE1": (3.1685523986816406, -1.2966246604919434, -0.1717153936624527), + "NE2": (2.947425603866577, 0.9601329565048218, -0.6888364553451538), + }, + "GLU": { + "N": (-1.5850872993469238, -1.337684154510498, 0.9490851163864136), + "CA": (-1.0560977458953857, 0.027459044009447098, 1.0306966304779053), + "C": (-1.7741456031799316, 0.9664392471313477, 0.09259600937366486), + "O": (-1.9012441635131836, 2.181349992752075, 0.402479350566864), + "CB": (0.4706551432609558, 0.048803869634866714, 0.8114414811134338), + "CG": (0.9133604764938354, -0.4219329059123993, -0.5830985307693481), + "CD": (2.398822069168091, -0.3097084164619446, -0.7210537791252136), + "OE1": (3.1389315128326416, -1.274524450302124, -0.39029765129089355), + "OE2": (2.9647817611694336, 0.8781346082687378, -1.1732689142227173), + }, + "GLY": { + "N": (-1.3942985534667969, -0.39875128865242004, -0.3370324671268463), + "CA": (-0.39974430203437805, 0.5488945245742798, 0.15242962539196014), + "C": (0.9440054893493652, -0.10314033925533295, 0.19859643280506134), + "O": (1.3352899551391602, -0.669218122959137, 1.2541258335113525), + }, + "HIS": { + "N": (-1.4532867670059204, -1.0689626932144165, 0.881072461605072), + "CA": (-1.3396095037460327, 0.24797579646110535, 0.24960045516490936), + "C": (-2.675257921218872, 0.6571555733680725, -0.30441102385520935), + "O": (-3.1311378479003906, 1.8079776763916016, -0.06785715371370316), + "CB": (-0.3041955828666687, 0.21721023321151733, -0.8885309100151062), + "CG": (1.0887513160705566, 0.028941065073013306, -0.36419469118118286), + "ND1": (1.840459942817688, 1.0411773920059204, 0.29804590344429016), + "CD2": (1.780855417251587, -1.1011489629745483, -0.3814258575439453), + "CE1": (2.9566943645477295, 0.4924798905849457, 0.6477115750312805), + "NE2": (3.0280203819274902, -0.8751969337463379, 0.26084381341934204), + }, + "ILE": { + "N": (-0.7167549729347229, -1.5426139831542969, -0.9983330368995667), + "CA": (-1.0636085271835327, -0.35169270634651184, -0.21393552422523499), + "C": (-1.3896740674972534, 0.8142145276069641, -1.1164065599441528), + "O": (-1.2377792596817017, 0.7302915453910828, -2.3656840324401855), + "CB": (0.061667006462812424, 0.01599610224366188, 0.8057394623756409), + "CG1": (1.502519965171814, -0.08899776637554169, 0.24154816567897797), + "CG2": (-0.053174979984760284, -0.8521055579185486, 2.0702083110809326), + "CD1": (1.7929610013961792, 0.899773120880127, -0.8863027691841125), + }, + "LEU": { + "N": (1.9657520055770874, -1.9763224124908447, -0.18391533195972443), + "CA": (1.3077669143676758, -0.6677430868148804, -0.19492436945438385), + "C": (1.9905058145523071, 0.24182087182998657, 0.7879968285560608), + "O": (2.06896710395813, -0.07880014181137085, 2.0048046112060547), + "CB": (-0.20306941866874695, -0.8093230128288269, 0.11243502795696259), + "CG": (-0.9916267395019531, 0.5234957337379456, 0.06723011285066605), + "CD1": (-2.4228057861328125, 0.29949337244033813, 0.573042094707489), + "CD2": (-1.0282856225967407, 1.1250264644622803, -1.346014380455017), + }, + "LYS": { + "N": (2.4221372604370117, -0.6473312377929688, 0.6370573043823242), + "CA": (2.0314927101135254, 0.2786507308483124, -0.4298512041568756), + "C": (2.7168593406677246, 1.595757246017456, -0.20924785733222961), + "O": (3.397681713104248, 2.116427421569824, -1.1332510709762573), + "CB": (0.5018402934074402, 0.4873858690261841, -0.49062973260879517), + "CG": (-0.25062066316604614, -0.7894009947776794, -0.9055535793304443), + "CD": (-1.769762635231018, -0.5552700161933899, -1.040329933166504), + "CE": (-2.576533555984497, -1.0221366882324219, 0.18493641912937164), + "NZ": (-2.269151210784912, -0.24293844401836395, 1.3849012851715088), + }, + "MET": { + "N": (1.8903918266296387, -1.5252995491027832, -0.42638593912124634), + "CA": (1.2630571126937866, -0.24417810142040253, -0.7626462578773499), + "C": (2.30391001701355, 0.8367712497711182, -0.7254616618156433), + "O": (2.465414524078369, 1.5928632020950317, -1.7207728624343872), + "CB": (0.10567972809076309, 0.10861825942993164, 0.19741646945476532), + "CG": (-1.0658042430877686, -0.8736631274223328, 0.08811883628368378), + "SD": (-2.4557132720947266, -0.3332225978374481, 1.1461700201034546), + "CE": (-3.265165090560913, 0.7033554911613464, -0.11588376015424728), + }, + "PHE": { + "N": (-2.8484435081481934, -1.525790810585022, 0.01789816841483116), + "CA": (-1.591969609260559, -0.8545162677764893, 0.35214468836784363), + "C": (-1.8900631666183472, 0.45833414793014526, 1.0232222080230713), + "O": (-1.3424992561340332, 0.74432373046875, 2.121629476547241), + "CB": (-0.760358452796936, -0.6342853307723999, -0.9257160425186157), + "CG": (0.604112982749939, -0.07200468331575394, -0.6148118376731873), + "CD1": (0.8468314409255981, 1.2480632066726685, -0.7146694660186768), + "CD2": (1.6827683448791504, -0.9758077263832092, -0.1423054188489914), + "CE1": (2.1801748275756836, 1.7875733375549316, -0.3744623064994812), + "CE2": (2.888307809829712, -0.48277512192726135, 0.16804970800876617), + "CZ": (3.149812936782837, 0.9656873941421509, 0.04440271109342575), + }, + "PRO": { + "N": (-0.836250364780426, -0.9899801015853882, 0.5561304688453674), + "CA": (0.32722190022468567, -0.6164458394050598, -0.25072571635246277), + "C": (1.6121541261672974, -1.1711241006851196, 0.31082412600517273), + "O": (1.6127740144729614, -2.2771971225738525, 0.9156193733215332), + "CB": (0.3248198926448822, 0.9028244018554688, -0.33368146419525146), + "CG": (-1.1425083875656128, 1.2730128765106201, -0.2590600252151489), + "CD": (-1.8495968580245972, 0.026575811207294464, 0.2681289613246918), + }, + "SER": { + "N": (0.674650251865387, 1.5018702745437622, -0.5367295145988464), + "CA": (0.00013792862591799349, 0.4966467022895813, 0.28510504961013794), + "C": (0.9941009879112244, -0.5374617576599121, 0.73505038022995), + "O": (1.0545241832733154, -0.8683545589447021, 1.9495396614074707), + "CB": (-1.1279288530349731, -0.1659376323223114, -0.5160963535308838), + "OG": (-1.8135979175567627, -1.085249662399292, 0.28947514295578003), + }, + "THR": { + "N": (-1.325830340385437, -1.3728225231170654, 0.6882233023643494), + "CA": (-0.5433306097984314, -0.16364754736423492, 0.41697052121162415), + "C": (-1.294381856918335, 0.7077372074127197, -0.5549946427345276), + "O": (-1.6939635276794434, 0.23654410243034363, -1.6540418863296509), + "CB": (0.853203296661377, -0.5363803505897522, -0.14109353721141815), + "OG1": (1.5220820903778076, -1.379003643989563, 0.7635167837142944), + "CG2": (1.7225933074951172, 0.7054727077484131, -0.3651331067085266), + }, + "TRP": { + "N": (3.686030864715576, 0.7599999904632568, 0.496155709028244), + "CA": (2.384092092514038, 0.09079249948263168, 0.5325262546539307), + "C": (2.1113572120666504, -0.6121063232421875, -0.7733646035194397), + "O": (1.796526312828064, -1.8323148488998413, -0.7775964140892029), + "CB": (1.281521201133728, 1.1139036417007446, 0.8559791445732117), + "CG": (-0.04292375594377518, 0.44645074009895325, 1.0942792892456055), + "CD1": (-0.42329534888267517, -0.15470874309539795, 2.2227554321289062), + "CD2": (-1.1023900508880615, 0.2158389836549759, 0.11529432237148285), + "NE1": (-1.7030320167541504, -0.7665823101997375, 2.0595016479492188), + "CE2": (-2.045644998550415, -0.4881173074245453, 0.710669219493866), + "CE3": (-1.2173502445220947, 0.6102271676063538, -1.300106406211853), + "CZ2": (-3.256009340286255, -0.9164394736289978, -0.00984987337142229), + "CZ3": (-2.315925121307373, 0.2306906282901764, -1.9776310920715332), + "CH2": (-3.3817875385284424, -0.5677337646484375, -1.3032053709030151), + }, + "TYR": { + "N": (-1.7900604009628296, -0.8409399390220642, 1.3180142641067505), + "CA": (-1.913882851600647, 0.23552845418453217, 0.330669641494751), + "C": (-3.347280740737915, 0.3588399887084961, -0.09830684959888458), + "O": (-3.967811346054077, -0.6449354290962219, -0.5423302054405212), + "CB": (-1.0093992948532104, 0.0004731413209810853, -0.8981552124023438), + "CG": (0.4520410895347595, 0.021162061020731926, -0.5305932760238647), + "CD1": (1.0992432832717896, 1.1877919435501099, -0.3579142987728119), + "CD2": (1.1803174018859863, -1.253401279449463, -0.31122180819511414), + "CE1": (2.5253450870513916, 1.1990256309509277, 0.029804613441228867), + "CE2": (2.471151113510132, -1.240687608718872, 0.043534230440855026), + "CZ": (3.180687665939331, 0.04672492295503616, 0.2214856892824173), + "OH": (4.523719787597656, 0.0671030730009079, 0.5877485871315002), + }, + "VAL": { + "N": (0.5987519025802612, -1.569443702697754, -0.7379124760627747), + "CA": (0.6014357209205627, -0.10503966361284256, -0.6336286664009094), + "C": (1.8391697406768799, 0.4067850410938263, 0.06351757049560547), + "O": (2.3952062129974365, -0.2666190266609192, 0.9731166958808899), + "CB": (-0.694736897945404, 0.4259096384048462, 0.03581475466489792), + "CG1": (-1.9276031255722046, 0.09515828639268875, -0.8172357082366943), + "CG2": (-0.8938426971435547, -0.08640842139720917, 1.472349762916565), + }, + "UNK": { + "N": (0.0, 0.0, 0.0), + "CA": (0.0, 0.0, 0.0), + "C": (0.0, 0.0, 0.0), + "O": (0.0, 0.0, 0.0), + }, +} + +# Protonated nitrogens at physiological pH (matches CHARGED_ATOMS in the +# opensource constants for the protein subset). +PROTEIN_CHARGED_ATOMS: dict[tuple[str, str], int] = { + ("LYS", "NZ"): 1, + ("ARG", "NH2"): 1, + ("HIS", "ND1"): 1, +} + +# Only the elements that appear in canonical protein heavy atoms. +_PROTEIN_ELEMENT_TO_ATOMIC_NUM: dict[str, int] = {"C": 6, "N": 7, "O": 8, "S": 16} + + +def _encode_atom_name(name: str) -> list[int]: + padded = name.ljust(4)[:4] + return [ord(c) - 32 if c != " " else 0 for c in padded] + + +def prepare_protein_features(sequence: str) -> dict[str, Tensor]: + """Featurize a single protein sequence for ESMFold2ExperimentalModel.forward. + + Returns the same keys with the same dtypes/shapes as + ``ESMFold2InputBuilder.prepare_input(StructurePredictionInput(...))`` + restricted to a single-chain protein with no MSA, modifications, + distogram conditioning, or covalent bonds. All tensors have a + leading batch dim of 1; the caller is responsible for moving them + to the model device. + """ + if not sequence: + raise ValueError("sequence must be non-empty") + + res_3letter = [PROTEIN_1TO3.get(c, "UNK") for c in sequence] + L = len(sequence) + + token_atom_starts: list[int] = [] + atom_records: list[tuple[int, str, str, int, tuple[float, float, float]]] = [] + res_type_vals: list[int] = [] + input_id_vals: list[int] = [] + distogram_rep_atom_idx: list[int] = [] + + atom_cursor = 0 + for t_idx, (letter, res_3) in enumerate(zip(sequence, res_3letter)): + atom_names = PROTEIN_HEAVY_ATOMS[res_3] + res_type = PROTEIN_RESIDUE_TO_RES_TYPE.get(res_3, PROTEIN_UNK_RES_TYPE) + input_id = ESM_PROTEIN_VOCAB.get(letter, ESM_PROTEIN_VOCAB["X"]) + + token_atom_starts.append(atom_cursor) + for name in atom_names: + charge = PROTEIN_CHARGED_ATOMS.get((res_3, name), 0) + element = name[0] # protein heavy atoms are all single-letter C/N/O/S + ref_pos = PROTEIN_REF_POS[res_3][name] + atom_records.append((t_idx, name, element, charge, ref_pos)) + atom_cursor += 1 + + rep_name = "CB" if "CB" in atom_names else "CA" + distogram_rep_atom_idx.append( + token_atom_starts[t_idx] + atom_names.index(rep_name) + ) + + res_type_vals.append(res_type) + input_id_vals.append(input_id) + + n_real_atoms = len(atom_records) + n_atoms = math.ceil(n_real_atoms / 32) * 32 if n_real_atoms > 0 else 32 + + ref_pos = torch.zeros(n_atoms, 3, dtype=torch.float32) + ref_element = torch.zeros(n_atoms, dtype=torch.int64) + ref_charge = torch.zeros(n_atoms, dtype=torch.int8) + ref_atom_name_chars = torch.zeros(n_atoms, 4, dtype=torch.int64) + ref_space_uid = torch.zeros(n_atoms, dtype=torch.int64) + atom_attention_mask = torch.zeros(n_atoms, dtype=torch.bool) + atom_to_token = torch.zeros(n_atoms, dtype=torch.int64) + + for i, (t_idx, name, element, charge, pos) in enumerate(atom_records): + ref_pos[i] = torch.tensor(pos, dtype=torch.float32) + ref_element[i] = _PROTEIN_ELEMENT_TO_ATOMIC_NUM[element] + ref_charge[i] = charge + ref_atom_name_chars[i] = torch.tensor( + _encode_atom_name(name), dtype=torch.int64 + ) + ref_space_uid[i] = t_idx + atom_attention_mask[i] = True + atom_to_token[i] = t_idx + + token_index = torch.arange(L, dtype=torch.int64) + residue_index = torch.arange(L, dtype=torch.int64) + asym_id = torch.zeros(L, dtype=torch.int64) + sym_id = torch.zeros(L, dtype=torch.int64) + entity_id = torch.ones(L, dtype=torch.int64) + mol_type = torch.full((L,), MOL_TYPE_PROTEIN, dtype=torch.int64) + res_type = torch.tensor(res_type_vals, dtype=torch.int64) + input_ids = torch.tensor(input_id_vals, dtype=torch.int64) + token_bonds = torch.zeros(L, L, 1, dtype=torch.float32) + token_attention_mask = torch.ones(L, dtype=torch.bool) + distogram_atom_idx = torch.tensor(distogram_rep_atom_idx, dtype=torch.int64) + + # Single-sequence MSA: depth 1, row 0 is the sequence itself. + msa = res_type.unsqueeze(0) + msa_attention_mask = torch.ones(1, L, dtype=torch.bool) + has_deletion = torch.zeros(1, L, dtype=torch.bool) + deletion_value = torch.zeros(1, L, dtype=torch.float32) + deletion_mean = torch.zeros(L, dtype=torch.float32) + + features = { + "token_index": token_index, + "residue_index": residue_index, + "asym_id": asym_id, + "sym_id": sym_id, + "entity_id": entity_id, + "mol_type": mol_type, + "res_type": res_type, + "input_ids": input_ids, + "token_bonds": token_bonds, + "token_attention_mask": token_attention_mask, + "ref_pos": ref_pos, + "ref_element": ref_element, + "ref_charge": ref_charge, + "ref_atom_name_chars": ref_atom_name_chars, + "ref_space_uid": ref_space_uid, + "atom_attention_mask": atom_attention_mask, + "atom_to_token": atom_to_token, + "distogram_atom_idx": distogram_atom_idx, + "msa": msa, + "msa_attention_mask": msa_attention_mask, + "has_deletion": has_deletion, + "deletion_value": deletion_value, + "deletion_mean": deletion_mean, + } + return {k: v.unsqueeze(0) for k, v in features.items()} + + +# 0-32 res_type → 3-letter name (only protein indices 2-22 are populated). +_RES_TYPE_TO_3LETTER: dict[int, str] = { + rt: three for three, rt in PROTEIN_RESIDUE_TO_RES_TYPE.items() +} +_RES_TYPE_TO_3LETTER[PROTEIN_UNK_RES_TYPE] = "UNK" + +# Featurization keys that ``output_to_pdb`` reads off the forward output. +# ``infer_protein`` re-attaches them because ``forward`` does not echo them +# back; both ESMFold2 model classes share this list. +OUTPUT_TO_PDB_FEATURE_KEYS: tuple[str, ...] = ( + "res_type", + "atom_to_token", + "ref_atom_name_chars", + "atom_attention_mask", + "token_attention_mask", + "residue_index", +) + + +def output_to_pdb(output: dict) -> str: + """Convert an ESMFold2 protein forward output into a PDB string. + + Expects ``output`` to carry the featurization keys re-attached by + ``infer_protein`` (``res_type``, ``atom_to_token``, + ``ref_atom_name_chars``, ``atom_attention_mask``, + ``token_attention_mask``, ``residue_index``) alongside the predicted + ``sample_atom_coords`` and ``plddt``. Builds a 37-atom + ``OFProtein`` (per-atom pLDDT in the b-factor column) and renders it + with the OpenFold utilities shipped in ``transformers.models.esm``. + """ + from transformers.models.esm.openfold_utils import OFProtein, to_pdb + from transformers.models.esm.openfold_utils import residue_constants as rc + + coords = output["sample_atom_coords"] + if coords.dim() == 4: + coords = coords[:, 0] + coords = coords.detach().cpu().numpy()[0] + + plddt = output["plddt"].detach().cpu().numpy()[0] + atom_to_token = output["atom_to_token"].cpu().numpy() + ref_chars = output["ref_atom_name_chars"].cpu().numpy() + res_type = output["res_type"].cpu().numpy() + token_mask = output["token_attention_mask"].cpu().numpy().astype(bool) + atom_mask_in = output["atom_attention_mask"].cpu().numpy().astype(bool) + residue_index_arr = output["residue_index"].cpu().numpy() + + if atom_to_token.ndim == 2: + atom_to_token = atom_to_token[0] + ref_chars = ref_chars[0] + res_type = res_type[0] + token_mask = token_mask[0] + atom_mask_in = atom_mask_in[0] + residue_index_arr = residue_index_arr[0] + + valid_tok = np.where(token_mask)[0] + n_res = valid_tok.shape[0] + + aatype = np.full(n_res, rc.restype_order_with_x["X"], dtype=np.int64) + for new_i, t in enumerate(valid_tok): + rt = int(res_type[t]) + three = _RES_TYPE_TO_3LETTER.get(rt) + if three is None or three == "UNK": + aatype[new_i] = rc.restype_order_with_x["X"] + else: + one = rc.restype_3to1.get(three, "X") + aatype[new_i] = rc.restype_order_with_x[one] + + atom_positions = np.zeros((n_res, 37, 3), dtype=np.float32) + atom_mask = np.zeros((n_res, 37), dtype=np.float32) + b_factors = np.zeros((n_res, 37), dtype=np.float32) + tok_to_new = {int(t): i for i, t in enumerate(valid_tok)} + + for a in range(atom_to_token.shape[0]): + if not atom_mask_in[a]: + continue + tok = int(atom_to_token[a]) + if tok not in tok_to_new: + continue + new_i = tok_to_new[tok] + name = "".join( + chr(int(c) + 32) if int(c) != 0 else " " for c in ref_chars[a] + ).strip() + idx37 = rc.atom_order.get(name) + if idx37 is None: + continue + atom_positions[new_i, idx37] = coords[a] + atom_mask[new_i, idx37] = 1.0 + b_factors[new_i, idx37] = float(plddt[tok]) + + pred = OFProtein( + aatype=aatype, + atom_positions=atom_positions, + atom_mask=atom_mask, + residue_index=residue_index_arr[valid_tok].astype(np.int32) + 1, + b_factors=b_factors, + ) + return to_pdb(pred) From 49c7a4a0e55975a7254e9e9967322bf36140e421 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 17:04:30 +0100 Subject: [PATCH 02/70] Strip vendored Triton kernels + set_kernel_backend selector from esmfold2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transformers loads fused kernels from the Hub via the `kernels` library (@use_kernel_forward_from_hub / register_kernel_mapping), not by vendoring Triton in a model dir. Remove the fork's bespoke acceleration stack and leave a single pure-PyTorch path; fused ops can return later via a Hub kernels repo as an opt-in follow-up. Removed: - src/transformers/models/esmfold2/kernels/ (8 Triton files, ~3.3k LOC). - The shared `set_kernel_backend` selector across the module tree, which drove BOTH backends: "fused" (vendored Triton) and "cuequivariance" (external lib). Both gone — the cueq import block and BACKEND_*/ _VALID_BACKENDS/_fused_active/_cueq_active helpers with them. - Per-module fused/cueq branches and now-dead helpers (_can_use_*, _fused_trimul_with_residual, split_kernel_weights, _kernel_flow_direction, Transition._swiglu_pre_w3/_addmm_residual, DropoutResidual fused impl). - The vestigial no-op set_kernel_backend hooks in distributed/utils.py and modeling_esmfold2_experimental.py. Kept intact: the independent `set_chunk_size` memory knob, and the optional flash-attn / transformer_engine guards. Verified: both modeling modules import; CPU smoke test runs AttentionPairBias, TriangleMultiplicativeUpdate (both orientations), Transition (chunked == unchunked), DropoutResidual, and FoldingTrunk end-to-end on the pure-PyTorch path. No new ruff errors introduced (9 pre-existing fork lint items remain for the later `make style` pass). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/distributed/utils.py | 22 +- .../models/esmfold2/kernels/__init__.py | 21 - .../kernels/fused_attention_pair_bias.py | 697 ------------------ .../kernels/fused_dropout_residual.py | 247 ------- .../esmfold2/kernels/fused_dual_gemm.py | 610 --------------- .../esmfold2/kernels/fused_ln_residual.py | 397 ---------- .../esmfold2/kernels/fused_lnlin_swiglu.py | 415 ----------- .../esmfold2/kernels/trimul_einsum_triton.py | 242 ------ .../esmfold2/kernels/trimul_with_residual.py | 622 ---------------- .../models/esmfold2/modeling_esmfold2.py | 26 +- .../esmfold2/modeling_esmfold2_common.py | 426 +---------- .../modeling_esmfold2_experimental.py | 10 - 12 files changed, 36 insertions(+), 3699 deletions(-) delete mode 100644 src/transformers/models/esmfold2/kernels/__init__.py delete mode 100644 src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py delete mode 100644 src/transformers/models/esmfold2/kernels/fused_dropout_residual.py delete mode 100644 src/transformers/models/esmfold2/kernels/fused_dual_gemm.py delete mode 100644 src/transformers/models/esmfold2/kernels/fused_ln_residual.py delete mode 100644 src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py delete mode 100644 src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py delete mode 100644 src/transformers/models/esmfold2/kernels/trimul_with_residual.py diff --git a/src/transformers/models/esmfold2/distributed/utils.py b/src/transformers/models/esmfold2/distributed/utils.py index a4ba6e7e6089..d2e1389c9dfb 100644 --- a/src/transformers/models/esmfold2/distributed/utils.py +++ b/src/transformers/models/esmfold2/distributed/utils.py @@ -399,13 +399,11 @@ def __init__(self, serial_trunk: nn.Module, dist_manager) -> None: f"expected DistributedManager, got {type(dist_manager).__name__}" ) - # ``FoldingTrunkDistributed`` requires the serial trunk's tri-mul - # kernels off and chunking disabled (it composes its own ring loop). - # ``serial_trunk`` is typed ``nn.Module`` (the SerialFoldingTrunk - # symbol is imported lazily inside the function to avoid a circular - # import with pairformer.py); pyright can't narrow through the - # lazy-import isinstance check. - serial_trunk.set_kernel_backend(None) # type: ignore[operator] + # ``FoldingTrunkDistributed`` requires the serial trunk's chunking + # disabled (it composes its own ring loop). ``serial_trunk`` is typed + # ``nn.Module`` (the SerialFoldingTrunk symbol is imported lazily inside + # the function to avoid a circular import with pairformer.py); pyright + # can't narrow through the lazy-import isinstance check. serial_trunk.set_chunk_size(None) # type: ignore[operator] self.dist_trunk = FoldingTrunkDistributed(serial_trunk, dist_manager) @@ -416,13 +414,9 @@ def __init__(self, serial_trunk: nn.Module, dist_manager) -> None: self.cp_axis_1 = self.device_mesh.size(2) self.shard_factor = lcm(self.cp_axis_0, self.cp_axis_1) - # The serial trunk exposes these knobs to the parent model. The - # distributed path doesn't support kernels/chunking, but the parent - # ``set_kernel_backend`` / ``set_chunk_size`` calls still need a no-op - # hook so they don't blow up. - def set_kernel_backend(self, _backend: str | None) -> None: - return - + # The serial trunk exposes this knob to the parent model. The distributed + # path doesn't support chunking, but the parent ``set_chunk_size`` call + # still needs a no-op hook so it doesn't blow up. def set_chunk_size(self, _chunk_size: int | None) -> None: return diff --git a/src/transformers/models/esmfold2/kernels/__init__.py b/src/transformers/models/esmfold2/kernels/__init__.py deleted file mode 100644 index 6705f4936c9b..000000000000 --- a/src/transformers/models/esmfold2/kernels/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# coding=utf-8 -# Copyright 2026 Biohub. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -"""Triton inference kernels for ESMFold2.""" - -from .fused_attention_pair_bias import fused_pair_bias -from .fused_dropout_residual import FusedDropoutResidual -from .fused_lnlin_swiglu import FusedLNLinearSwiGLU -from .trimul_with_residual import triangle_multiplicative_update_with_residual - -__all__ = [ - "fused_pair_bias", - "FusedDropoutResidual", - "FusedLNLinearSwiGLU", - "triangle_multiplicative_update_with_residual", -] diff --git a/src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py b/src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py deleted file mode 100644 index 98b722397bb7..000000000000 --- a/src/transformers/models/esmfold2/kernels/fused_attention_pair_bias.py +++ /dev/null @@ -1,697 +0,0 @@ -"""Fused Triton kernel for AttentionPairBias (forward + backward). - -Inspired by cuequivariance's ``attention_pair_bias`` -(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently -re-implemented in Triton with a backward pass and no sequence-length gate. - -Fuses ``LayerNorm(z) -> z @ w_proj_z.T -> (1-mask)*(-INF)`` into a single -kernel that emits a ``bias[B, H, Q, K]`` tensor, then dispatches the -attention itself to ``torch.nn.functional.scaled_dot_product_attention``. -""" - -# ruff: noqa: E402 - -import os -import warnings - -warnings.filterwarnings("ignore", category=FutureWarning) -warnings.filterwarnings("ignore", category=UserWarning) -warnings.filterwarnings("ignore", category=DeprecationWarning) -os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1") -os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1") - -import torch -import triton -import triton.language as tl - -# Static config — runtime autotune cold-start is unshippable for inference. -_BIAS_AUTOTUNE_CONFIGS = [ - triton.Config({"TILE_K": 64, "TILE_C": 64}, num_stages=3, num_warps=4) -] - - -@triton.autotune( - configs=_BIAS_AUTOTUNE_CONFIGS, - key=["Q", "K", "DIM_Z", "NUM_HEADS", "HEADS_PER_BLK", "HAS_MASK", "AFFINE"], -) -@triton.jit -def _pair_bias_kernel( - z_ptr, # [B, Q, K, DIM_Z] pair tensor, bf16/fp16/fp32 - mask_ptr, # [B, K] key padding mask (bool/uint8) - w_proj_z_ptr, # [NUM_HEADS, DIM_Z] bias-projection weight - w_ln_ptr, # [DIM_Z] LN gamma (or unused) - b_ln_ptr, # [DIM_Z] LN beta (or unused) - out_ptr, # [B, NUM_HEADS, Q, K] fused output bias, same dtype as z - mean_ptr, # [B, Q, K] saved LN mean (fp32) or dummy - rstd_ptr, # [B, Q, K] saved LN rstd (fp32) or dummy - B, - Q, - K, - NEG_INF: tl.constexpr, - EPS: tl.constexpr, - DIM_Z: tl.constexpr, - DIM_Z_PAD: tl.constexpr, - NUM_HEADS: tl.constexpr, - HEADS_PER_BLK: tl.constexpr, - TILE_K: tl.constexpr, - TILE_C: tl.constexpr, # tile over the DIM_Z reduction axis - HAS_MASK: tl.constexpr, - AFFINE: tl.constexpr, - SAVE_STATS: tl.constexpr, -): - """One CTA owns (one Q-row × TILE_K K-positions × HEADS_PER_BLK heads). - - Grid: (cdiv(K, TILE_K), Q, B * cdiv(NUM_HEADS, HEADS_PER_BLK)). - - Each thread block computes a tile of the output bias - ``bias[b, h_blk, q, k_tile]`` and stores it to ``out_ptr``. Layout of - ``out_ptr`` is ``[B, NUM_HEADS, Q, K]`` so the downstream SDPA call can - pass it directly as ``attn_mask`` (broadcast-compatible with the standard - BHQK attention layout). - - When ``SAVE_STATS`` is set (training path), the per-row ``mean`` and - ``rstd`` are stored into ``mean_ptr``/``rstd_ptr`` of shape ``[B, Q, K]`` - so the backward kernel can avoid recomputing them. Only the first head-block - in each (B, Q, K) tile writes the stats to avoid races. - """ - pid_k = tl.program_id(0) - pid_q = tl.program_id(1) - pid_bh = tl.program_id(2) - NUM_HEAD_BLKS: tl.constexpr = (NUM_HEADS + HEADS_PER_BLK - 1) // HEADS_PER_BLK - pid_b = pid_bh // NUM_HEAD_BLKS - pid_hblk = pid_bh % NUM_HEAD_BLKS - - offs_k = pid_k * TILE_K + tl.arange(0, TILE_K) - offs_z_full = tl.arange(0, DIM_Z_PAD) - offs_h = pid_hblk * HEADS_PER_BLK + tl.arange(0, HEADS_PER_BLK) - mask_k = offs_k < K - mask_h = offs_h < NUM_HEADS - - z_full_ptrs = ( - z_ptr - + pid_b * Q * K * DIM_Z - + pid_q * K * DIM_Z - + offs_k[:, None] * DIM_Z - + offs_z_full[None, :] - ) - mask_z_full = offs_z_full < DIM_Z - z_full = tl.load( - z_full_ptrs, mask=mask_k[:, None] & mask_z_full[None, :], other=0.0 - ).to(tl.float32) - - mean = tl.sum(z_full, axis=1) / DIM_Z - z_centered = z_full - mean[:, None] - z_centered = tl.where(mask_z_full[None, :], z_centered, 0.0) - var = tl.sum(z_centered * z_centered, axis=1) / DIM_Z - rstd = 1.0 / tl.sqrt(var + EPS) - - # Save mean/rstd for backward — only the first head-block writes, all - # head-blocks have the same value so this avoids racy duplicate writes. - if SAVE_STATS: - if pid_hblk == 0: - stats_ptrs = mean_ptr + pid_b * Q * K + pid_q * K + offs_k - tl.store(stats_ptrs, mean, mask=mask_k) - stats_ptrs2 = rstd_ptr + pid_b * Q * K + pid_q * K + offs_k - tl.store(stats_ptrs2, rstd, mask=mask_k) - - acc = tl.zeros([TILE_K, HEADS_PER_BLK], dtype=tl.float32) - num_tiles_c = tl.cdiv(DIM_Z, TILE_C) - for tc in range(0, num_tiles_c): - offs_c = tc * TILE_C + tl.arange(0, TILE_C) - mask_c = offs_c < DIM_Z - - z_slice_ptrs = ( - z_ptr - + pid_b * Q * K * DIM_Z - + pid_q * K * DIM_Z - + offs_k[:, None] * DIM_Z - + offs_c[None, :] - ) - z_slice = tl.load( - z_slice_ptrs, mask=mask_k[:, None] & mask_c[None, :], other=0.0 - ).to(tl.float32) - - z_norm = (z_slice - mean[:, None]) * rstd[:, None] - if AFFINE: - gamma = tl.load(w_ln_ptr + offs_c, mask=mask_c, other=1.0).to(tl.float32) - beta = tl.load(b_ln_ptr + offs_c, mask=mask_c, other=0.0).to(tl.float32) - z_norm = z_norm * gamma[None, :] + beta[None, :] - z_norm = tl.where(mask_c[None, :], z_norm, 0.0) - - w_ptrs = w_proj_z_ptr + (offs_h[None, :] * DIM_Z + offs_c[:, None]) - w_tile = tl.load(w_ptrs, mask=mask_h[None, :] & mask_c[:, None], other=0.0).to( - tl.float32 - ) - - acc = tl.dot(z_norm.to(tl.float32), w_tile, acc, input_precision="tf32x3") - - if HAS_MASK: - m_tile = tl.load(mask_ptr + pid_b * K + offs_k, mask=mask_k, other=0).to( - tl.int32 - ) - acc = acc + tl.where(m_tile == 0, NEG_INF, 0.0)[:, None] - # Mask out-of-bounds K positions (we pad to TILE_K). - acc = tl.where(mask_k[:, None], acc, NEG_INF) - - out_ptrs = ( - out_ptr - + pid_b * NUM_HEADS * Q * K - + offs_h[None, :] * Q * K - + pid_q * K - + offs_k[:, None] - ) - tl.store( - out_ptrs, - acc.to(out_ptr.type.element_ty), - mask=mask_k[:, None] & mask_h[None, :], - ) - - -# Backward is reduction-heavy (atomics into d_w_proj_z / d_pair_norm_*); modest -# TILE_K=32 balances ILP and register pressure on Hopper. -_BIAS_BWD_CONFIGS = [triton.Config({"TILE_K": 32}, num_stages=3, num_warps=4)] - - -@triton.autotune( - configs=_BIAS_BWD_CONFIGS, key=["Q", "K", "DIM_Z", "NUM_HEADS", "AFFINE"] -) -@triton.jit -def _pair_bias_backward_kernel( - z_ptr, # [B, Q, K, DIM_Z] input pair tensor (bf16) - w_proj_z_ptr, # [NUM_HEADS, DIM_Z] bias-projection weight (bf16) - w_ln_ptr, # [DIM_Z] LN gamma (bf16, or dummy) - b_ln_ptr, # [DIM_Z] LN beta (bf16, or dummy) - mean_ptr, # [B, Q, K] saved LN mean (fp32) - rstd_ptr, # [B, Q, K] saved LN rstd (fp32) - d_bias_ptr, # [B, NUM_HEADS, Q, K] upstream gradient (bf16) - d_z_ptr, # [B, Q, K, DIM_Z] output d_z (bf16) - d_w_proj_z_ptr, # [NUM_HEADS, DIM_Z] output d_w_proj_z (fp32 accum) - d_ln_w_ptr, # [DIM_Z] output d_pair_norm_w (fp32 accum) - d_ln_b_ptr, # [DIM_Z] output d_pair_norm_b (fp32 accum) - Q, - K, - EPS: tl.constexpr, - DIM_Z: tl.constexpr, - DIM_Z_PAD: tl.constexpr, - NUM_HEADS: tl.constexpr, - NUM_HEADS_PAD: tl.constexpr, - TILE_K: tl.constexpr, - AFFINE: tl.constexpr, -): - """Backward for ``fused_pair_bias``. - - Grid: (cdiv(K, TILE_K), Q, B). - - Each CTA processes a (b, q, k_tile) slab and: - - 1. Loads ``z[b,q,k,:]``, ``mean``, ``rstd``, ``d_bias[b,:,q,k]``, - ``w_proj_z[:,:]``, ``gamma``, ``beta``. - 2. Computes ``z_hat = (z - mean) * rstd``, ``z_norm = z_hat * gamma + beta``. - 3. ``d_z_norm[k,c] = sum_h d_bias[h,k] * w_proj_z[h,c]`` (matmul, k×c). - 4. Atomic-add ``d_w_proj_z[h,c] += d_bias[k,h]^T @ z_norm[k,c]``. - 5. Atomic-add ``d_pair_norm_b[c] += sum_k d_z_norm[k,c]``. - 6. Atomic-add ``d_pair_norm_w[c] += sum_k d_z_norm[k,c] * z_hat[k,c]``. - 7. ``d_z_hat = d_z_norm * gamma``. - 8. LN bwd: ``d_z = (d_z_hat - mean_c(d_z_hat) - z_hat * mean_c(d_z_hat * z_hat)) * rstd``. - 9. Store ``d_z``. - - All math is fp32 internally; only loads/stores are bf16. Atomic adds - target fp32 buffers (bf16 atomics are not supported on Hopper). - """ - pid_k = tl.program_id(0) - pid_q = tl.program_id(1) - pid_b = tl.program_id(2) - - offs_k = pid_k * TILE_K + tl.arange(0, TILE_K) - offs_z = tl.arange(0, DIM_Z_PAD) - offs_h = tl.arange(0, NUM_HEADS_PAD) - mask_k = offs_k < K - mask_z = offs_z < DIM_Z - mask_h = offs_h < NUM_HEADS - - z_ptrs = ( - z_ptr - + pid_b * Q * K * DIM_Z - + pid_q * K * DIM_Z - + offs_k[:, None] * DIM_Z - + offs_z[None, :] - ) - z = tl.load(z_ptrs, mask=mask_k[:, None] & mask_z[None, :], other=0.0).to( - tl.float32 - ) - mean_ptrs = mean_ptr + pid_b * Q * K + pid_q * K + offs_k - rstd_ptrs = rstd_ptr + pid_b * Q * K + pid_q * K + offs_k - mean = tl.load(mean_ptrs, mask=mask_k, other=0.0) - rstd = tl.load(rstd_ptrs, mask=mask_k, other=0.0) - - z_hat = (z - mean[:, None]) * rstd[:, None] - z_hat = tl.where(mask_k[:, None] & mask_z[None, :], z_hat, 0.0) - - if AFFINE: - gamma = tl.load(w_ln_ptr + offs_z, mask=mask_z, other=1.0).to(tl.float32) - beta = tl.load(b_ln_ptr + offs_z, mask=mask_z, other=0.0).to(tl.float32) - else: - gamma = tl.full([DIM_Z_PAD], 1.0, dtype=tl.float32) - beta = tl.full([DIM_Z_PAD], 0.0, dtype=tl.float32) - # Recompute normalized output (needed for d_w_proj_z). - z_norm = z_hat * gamma[None, :] + beta[None, :] - z_norm = tl.where(mask_k[:, None] & mask_z[None, :], z_norm, 0.0) - - d_bias_ptrs = ( - d_bias_ptr - + pid_b * NUM_HEADS * Q * K - + offs_h[None, :] * Q * K - + pid_q * K - + offs_k[:, None] - ) - d_bias = tl.load(d_bias_ptrs, mask=mask_k[:, None] & mask_h[None, :], other=0.0).to( - tl.float32 - ) - - w_proj_ptrs = w_proj_z_ptr + offs_h[:, None] * DIM_Z + offs_z[None, :] - w_proj = tl.load(w_proj_ptrs, mask=mask_h[:, None] & mask_z[None, :], other=0.0).to( - tl.float32 - ) - - d_z_norm = tl.dot(d_bias, w_proj, input_precision="tf32x3") - d_z_norm = tl.where(mask_k[:, None] & mask_z[None, :], d_z_norm, 0.0) - - d_w = tl.dot(tl.trans(d_bias), z_norm, input_precision="tf32x3") - d_w_ptrs = d_w_proj_z_ptr + offs_h[:, None] * DIM_Z + offs_z[None, :] - tl.atomic_add(d_w_ptrs, d_w, mask=mask_h[:, None] & mask_z[None, :], sem="relaxed") - - if AFFINE: - d_b_tile = tl.sum(d_z_norm, axis=0) - tl.atomic_add(d_ln_b_ptr + offs_z, d_b_tile, mask=mask_z, sem="relaxed") - - d_w_tile = tl.sum(d_z_norm * z_hat, axis=0) - tl.atomic_add(d_ln_w_ptr + offs_z, d_w_tile, mask=mask_z, sem="relaxed") - - d_z_hat = d_z_norm * gamma[None, :] - d_z_hat = tl.where(mask_k[:, None] & mask_z[None, :], d_z_hat, 0.0) - sum_dzh = tl.sum(d_z_hat, axis=1) - sum_dzh_zhat = tl.sum(d_z_hat * z_hat, axis=1) - mean_dzh = sum_dzh / DIM_Z - mean_dzh_zhat = sum_dzh_zhat / DIM_Z - d_z = (d_z_hat - mean_dzh[:, None] - z_hat * mean_dzh_zhat[:, None]) * rstd[:, None] - d_z = tl.where(mask_k[:, None] & mask_z[None, :], d_z, 0.0) - - d_z_ptrs = ( - d_z_ptr - + pid_b * Q * K * DIM_Z - + pid_q * K * DIM_Z - + offs_k[:, None] * DIM_Z - + offs_z[None, :] - ) - tl.store( - d_z_ptrs, - d_z.to(d_z_ptr.type.element_ty), - mask=mask_k[:, None] & mask_z[None, :], - ) - - -def _next_pow2(x: int) -> int: - p = 1 - while p < x: - p *= 2 - return max(p, 16) - - -def _round_up_heads_per_blk(num_heads: int) -> int: - """Always return 16 — ``tl.dot`` requires M, N, K >= 16 on Hopper. - - For the AF3-style transformer ``num_heads=16``; ``HEADS_PER_BLK=16`` gives - a single CTA per (b, q, k_tile) and is the cheapest schedule. Head-counts - below 16 are zero-padded inside the kernel. - """ - del num_heads # head-tile is fixed at 16 by the tl.dot lower bound - return 16 - - -def _launch_forward( - z: torch.Tensor, - mask: torch.Tensor | None, - w_proj_z: torch.Tensor, - pair_norm_w: torch.Tensor | None, - pair_norm_b: torch.Tensor | None, - num_heads: int, - eps: float, - inf: float, - save_stats: bool, -) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]: - """Forward kernel launch helper. Returns ``(bias, mean, rstd)``. - - ``mean``/``rstd`` are ``None`` unless ``save_stats=True``. - """ - assert z.dim() == 4, f"z must be (B,Q,K,DIM_Z); got {z.shape}" - B, Q, K, DIM_Z = z.shape - assert w_proj_z.shape == ( - num_heads, - DIM_Z, - ), f"w_proj_z {w_proj_z.shape} ≠ ({num_heads}, {DIM_Z})" - - z = z.contiguous() - w_proj_z = w_proj_z.contiguous() - affine = pair_norm_w is not None or pair_norm_b is not None - if affine: - if pair_norm_w is None: - pair_norm_w = torch.ones(DIM_Z, device=z.device, dtype=z.dtype) - if pair_norm_b is None: - pair_norm_b = torch.zeros(DIM_Z, device=z.device, dtype=z.dtype) - pair_norm_w = pair_norm_w.contiguous() - pair_norm_b = pair_norm_b.contiguous() - if mask is not None: - mask = mask.contiguous() - assert mask.shape == (B, K), f"mask {mask.shape} ≠ ({B}, {K})" - - out = torch.empty((B, num_heads, Q, K), device=z.device, dtype=z.dtype) - if save_stats: - mean = torch.empty((B, Q, K), device=z.device, dtype=torch.float32) - rstd = torch.empty((B, Q, K), device=z.device, dtype=torch.float32) - else: - mean = None - rstd = None - heads_per_blk = _round_up_heads_per_blk(num_heads) - DIM_Z_PAD = _next_pow2(DIM_Z) - _dummy = torch.empty(1, device=z.device, dtype=z.dtype) - _dummy_f32 = torch.empty(1, device=z.device, dtype=torch.float32) - num_head_blks = (num_heads + heads_per_blk - 1) // heads_per_blk - grid = lambda meta: (triton.cdiv(K, meta["TILE_K"]), Q, B * num_head_blks) - _pair_bias_kernel[grid]( - z, - mask if mask is not None else _dummy, - w_proj_z, - pair_norm_w if affine else _dummy, - pair_norm_b if affine else _dummy, - out, - mean if save_stats else _dummy_f32, - rstd if save_stats else _dummy_f32, - B, - Q, - K, - NEG_INF=-float(inf), - EPS=eps, - DIM_Z=DIM_Z, - DIM_Z_PAD=DIM_Z_PAD, - NUM_HEADS=num_heads, - HEADS_PER_BLK=heads_per_blk, - HAS_MASK=mask is not None, - AFFINE=affine, - SAVE_STATS=save_stats, - ) - return out, mean, rstd - - -def _launch_backward( - z: torch.Tensor, - w_proj_z: torch.Tensor, - pair_norm_w: torch.Tensor | None, - pair_norm_b: torch.Tensor | None, - mean: torch.Tensor, - rstd: torch.Tensor, - d_bias: torch.Tensor, - eps: float, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None]: - """Backward kernel launch helper. Returns ``(d_z, d_w_proj_z, d_pair_norm_w, d_pair_norm_b)``. - - The latter two are ``None`` when affine is off. - """ - B, Q, K, DIM_Z = z.shape - NUM_HEADS = w_proj_z.shape[0] - affine = pair_norm_w is not None - if affine: - assert pair_norm_b is not None - - z = z.contiguous() - w_proj_z = w_proj_z.contiguous() - d_bias = d_bias.contiguous() - if affine: - assert pair_norm_w is not None and pair_norm_b is not None - pair_norm_w = pair_norm_w.contiguous() - pair_norm_b = pair_norm_b.contiguous() - - d_z = torch.empty_like(z) - # fp32 accumulators for atomic adds — bf16 atomic_add is not supported on Hopper - d_w_proj_z_f32 = torch.zeros( - (NUM_HEADS, DIM_Z), device=z.device, dtype=torch.float32 - ) - if affine: - d_pair_norm_w_f32 = torch.zeros(DIM_Z, device=z.device, dtype=torch.float32) - d_pair_norm_b_f32 = torch.zeros(DIM_Z, device=z.device, dtype=torch.float32) - else: - d_pair_norm_w_f32 = torch.zeros(1, device=z.device, dtype=torch.float32) - d_pair_norm_b_f32 = torch.zeros(1, device=z.device, dtype=torch.float32) - - DIM_Z_PAD = _next_pow2(DIM_Z) - # min 16 to satisfy Triton's tl.dot requirement (M, N, K >= 16 on Hopper). - NUM_HEADS_PAD = max(16, _next_pow2(NUM_HEADS)) - _dummy = torch.empty(1, device=z.device, dtype=z.dtype) - - grid = lambda meta: (triton.cdiv(K, meta["TILE_K"]), Q, B) - _pair_bias_backward_kernel[grid]( - z, - w_proj_z, - pair_norm_w if affine else _dummy, - pair_norm_b if affine else _dummy, - mean, - rstd, - d_bias, - d_z, - d_w_proj_z_f32, - d_pair_norm_w_f32, - d_pair_norm_b_f32, - Q, - K, - EPS=eps, - DIM_Z=DIM_Z, - DIM_Z_PAD=DIM_Z_PAD, - NUM_HEADS=NUM_HEADS, - NUM_HEADS_PAD=NUM_HEADS_PAD, - AFFINE=affine, - ) - - d_w_proj_z = d_w_proj_z_f32.to(w_proj_z.dtype) - d_pair_norm_w: torch.Tensor | None - d_pair_norm_b: torch.Tensor | None - if affine: - assert pair_norm_w is not None and pair_norm_b is not None - d_pair_norm_w = d_pair_norm_w_f32.to(pair_norm_w.dtype) - d_pair_norm_b = d_pair_norm_b_f32.to(pair_norm_b.dtype) - else: - d_pair_norm_w = None - d_pair_norm_b = None - - return d_z, d_w_proj_z, d_pair_norm_w, d_pair_norm_b - - -class FusedPairBias(torch.autograd.Function): - """Autograd wrapper around ``_pair_bias_kernel`` and ``_pair_bias_backward_kernel``. - - Forward saves ``(z, w_proj_z, pair_norm_w, pair_norm_b, mean, rstd)`` so that - the backward kernel can recompute ``z_hat`` without re-doing the full - LN reduction. ``mask`` is non-differentiable; we save it in ``ctx`` only as - a marker (the kernel applies it inside the forward, but mask positions get - -INF and so contribute 0 gradient through softmax → no special handling - needed for ``d_bias``). - """ - - @staticmethod - def forward(ctx, z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads, eps, inf): - out, mean, rstd = _launch_forward( - z, - mask, - w_proj_z, - pair_norm_w, - pair_norm_b, - num_heads, - eps, - inf, - save_stats=True, - ) - affine = pair_norm_w is not None or pair_norm_b is not None - ctx.save_for_backward( - z, - w_proj_z, - pair_norm_w if affine else None, - pair_norm_b if affine else None, - mean, - rstd, - ) - ctx.affine = affine - ctx.eps = eps - return out - - @staticmethod - def backward(ctx, d_bias): - z, w_proj_z, pair_norm_w, pair_norm_b, mean, rstd = ctx.saved_tensors - d_z, d_w_proj_z, d_pair_norm_w, d_pair_norm_b = _launch_backward( - z, - w_proj_z, - pair_norm_w, - pair_norm_b, - mean, - rstd, - d_bias.contiguous(), - ctx.eps, - ) - # Order must match forward args: (z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads, eps, inf) - return ( - d_z, - None, # mask - d_w_proj_z, - d_pair_norm_w, - d_pair_norm_b, - None, # num_heads - None, # eps - None, # inf - ) - - -@torch._dynamo.disable -def fused_pair_bias( - z: torch.Tensor, - mask: torch.Tensor | None, - w_proj_z: torch.Tensor, - pair_norm_w: torch.Tensor | None, - pair_norm_b: torch.Tensor | None, - *, - num_heads: int, - eps: float = 1e-5, - inf: float = 1e6, -) -> torch.Tensor: - """Compute ``bias[B, H, Q, K] = LN(z) @ w_proj_z.T + (1-mask)*-INF``. - - Dispatches to the autograd-aware ``FusedPairBias`` path when autograd is - enabled (i.e. any input requires_grad and we are not in a no_grad/inference - context). Otherwise falls back to the forward-only kernel. - - Parameters - ---------- - z : (B, Q, K, DIM_Z) - mask : (B, K) bool or None. True = keep, False = mask out (-INF added). - w_proj_z : (num_heads, DIM_Z) - pair_norm_w, pair_norm_b : (DIM_Z,) or None. Pass both or neither. - num_heads : int - eps, inf : LN epsilon and masking-infinity respectively. - - Returns - ------- - bias : (B, num_heads, Q, K) — same dtype as z. - """ - use_autograd = torch.is_grad_enabled() and ( - z.requires_grad - or w_proj_z.requires_grad - or (pair_norm_w is not None and pair_norm_w.requires_grad) - or (pair_norm_b is not None and pair_norm_b.requires_grad) - ) - if use_autograd: - out_t: torch.Tensor = FusedPairBias.apply( # type: ignore[assignment] - z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads, eps, inf - ) - return out_t - out, _, _ = _launch_forward( - z, - mask, - w_proj_z, - pair_norm_w, - pair_norm_b, - num_heads, - eps, - inf, - save_stats=False, - ) - return out - - -def fused_attention_pair_bias( - q: torch.Tensor, - k: torch.Tensor, - v: torch.Tensor, - z: torch.Tensor | None, - mask: torch.Tensor | None, - x_for_gate: torch.Tensor, - *, - w_proj_z: torch.Tensor | None, - w_proj_g: torch.Tensor, - w_proj_o: torch.Tensor, - pair_norm_w: torch.Tensor | None = None, - pair_norm_b: torch.Tensor | None = None, - eps: float = 1e-5, - inf: float = 1e6, - precomputed_bias: torch.Tensor | None = None, -) -> torch.Tensor: - """End-to-end fused AttentionPairBias forward (no-conditioning path). - - Pipeline: - bias = fused_pair_bias(z, mask, w_proj_z, ln_w, ln_b) # Triton - attn = SDPA(q, k, v, attn_mask=bias) # cuDNN / Flash - gate = sigmoid(linear(x_for_gate, w_proj_g)) # cuBLAS - out = linear(gate * attn, w_proj_o) # cuBLAS - - Parameters - ---------- - q, k, v : (B, H, Q|K, D) — already-projected query/key/value, head-split. - z : (B, Q, K, DIM_Z) — raw (unnormed) pair tensor. Ignored when - ``precomputed_bias`` is supplied. - mask : (B, K) bool or None. Ignored when ``precomputed_bias`` is supplied. - x_for_gate : (B, Q, d_model) — pre-norm input for the gate - ``sigmoid(x @ w_proj_g.T)``. In the production module this is the - adaln/pre_norm output ``x`` (same tensor that feeds ``q``). - w_proj_z : (H, DIM_Z). Ignored when ``precomputed_bias`` is supplied. - w_proj_g, w_proj_o : (d_model, d_model) - pair_norm_w, pair_norm_b : (DIM_Z,). Ignored when ``precomputed_bias`` is - supplied. - precomputed_bias : (B, H, Q, K) optional cached bias tensor. When provided - the LN+proj+mask Triton kernel is skipped entirely — the bias is reused - as the SDPA ``attn_mask`` directly. This is the 50× diffusion-step - amortization path: ``z`` is constant within a loop, so the bias can - be computed once and reused across all 50 denoise steps. Compute it - with ``fused_pair_bias`` once, then pass it back here on every step. - - Returns - ------- - out : (B, Q, d_model) - """ - B, H, Q, D = q.shape - d_model = H * D - - if precomputed_bias is not None: - assert ( - not torch.is_grad_enabled() - ), "precomputed_bias path is inference-only; autograd is not supported." - bias = precomputed_bias - else: - if z is None or w_proj_z is None: - raise ValueError( - "Either precomputed_bias OR (z, w_proj_z) must be supplied" - ) - bias = fused_pair_bias( - z, mask, w_proj_z, pair_norm_w, pair_norm_b, num_heads=H, eps=eps, inf=inf - ) # (B, H, Q, K) - - # Match the dtype expected by cuDNN attention. q/k/v are already bf16 in - # inference; ``bias`` inherits z's dtype. - if not torch.compiler.is_compiling(): - with torch.nn.attention.sdpa_kernel( - backends=[ - torch.nn.attention.SDPBackend.CUDNN_ATTENTION, - torch.nn.attention.SDPBackend.FLASH_ATTENTION, - torch.nn.attention.SDPBackend.EFFICIENT_ATTENTION, - ], - set_priority=True, - ): - attn = torch.nn.functional.scaled_dot_product_attention( - q, k, v, attn_mask=bias, is_causal=False - ) - else: - attn = torch.nn.functional.scaled_dot_product_attention( - q, k, v, attn_mask=bias, is_causal=False - ) - attn = attn.transpose(1, 2).contiguous().view(B, Q, d_model) - - gate = torch.sigmoid(torch.nn.functional.linear(x_for_gate, w_proj_g)) - out = torch.nn.functional.linear(gate * attn, w_proj_o) - return out - - -__all__ = ["FusedPairBias", "fused_attention_pair_bias", "fused_pair_bias"] diff --git a/src/transformers/models/esmfold2/kernels/fused_dropout_residual.py b/src/transformers/models/esmfold2/kernels/fused_dropout_residual.py deleted file mode 100644 index 2335d66fe895..000000000000 --- a/src/transformers/models/esmfold2/kernels/fused_dropout_residual.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Fused row-shared-dropout + residual-add kernel for the pair stream. - -For the pairformer pattern: - - pair_new = pair + Dropout(r, batch_dim=1)(delta) - -where `delta` is the output of e.g. a triangle multiplication, the row-shared -dropout multiplies `delta` by a Bernoulli mask of shape `[B, 1, N_col, D]` -(broadcast over the row dim) scaled by `1/(1-r)`. Naively this materializes -two intermediate `[B, N, N, D]` tensors (one for `delta * mask`, one for -`pair + ...`) — three full HBM round-trips of the pair tensor. - -This kernel reads `pair`, `delta`, and the small `[N_col, D]` shared mask -(via modulo-`N_col` indexing — no broadcast materialization) and writes the -combined `pair + delta * mask` once. - -Backward is also a single Triton kernel: it mutates the saved mask buffer in -place to produce `ddelta = dout * mask`, and the residual gradient passes -through unchanged. - -Convention: the mask is expected to already be scaled (i.e. it's the output -of `nn.Dropout(r)(ones_like(...))` so that retained entries have value -`1/(1-r)`). -""" - -from __future__ import annotations - -import torch -import torch.nn as nn -import triton -import triton.language as tl - - -@triton.jit -def _fused_dropout_residual_fwd_kernel( - pair_ptr, # [M, D] M = B*N_row*N_col - delta_ptr, # [M, D] - mask_ptr, # [B*N_col, D] per-batch row-shared mask - out_ptr, # [M, D] - M, - D: tl.constexpr, - N_COL: tl.constexpr, - STRIDE_B: tl.constexpr, # = N_row * N_col, used to recover batch index - BLOCK_M: tl.constexpr, - BLOCK_D: tl.constexpr, -): - pid_m = tl.program_id(0).to(tl.int64) - pid_d = tl.program_id(1).to(tl.int64) - - offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) - offs_d = pid_d * BLOCK_D + tl.arange(0, BLOCK_D) - mm_m = offs_m < M - mm_d = offs_d < D - - # m = b*N_row*N_col + i*N_col + j → b = m // (N_row*N_col), j = m % N_col - # mask is laid out as [B*N_col, D] so per-batch index is b*N_col + j. - b = offs_m // STRIDE_B - j = offs_m % N_COL - mask_row = b * N_COL + j - - pair_ptrs = pair_ptr + offs_m[:, None] * D + offs_d[None, :] - delta_ptrs = delta_ptr + offs_m[:, None] * D + offs_d[None, :] - out_ptrs = out_ptr + offs_m[:, None] * D + offs_d[None, :] - mask_ptrs = mask_ptr + mask_row[:, None] * D + offs_d[None, :] - - p = tl.load(pair_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) - de = tl.load(delta_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) - mk = tl.load(mask_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) - - o = p + de * mk - tl.store( - out_ptrs, o.to(out_ptr.type.element_ty), mask=mm_m[:, None] & mm_d[None, :] - ) - - -@triton.jit -def _fused_dropout_residual_bwd_kernel( - dout_ptr, # [M, D] - mask_ptr, # [B*N_col, D] - ddelta_ptr, # [M, D] - M, - D: tl.constexpr, - N_COL: tl.constexpr, - STRIDE_B: tl.constexpr, - BLOCK_M: tl.constexpr, - BLOCK_D: tl.constexpr, -): - pid_m = tl.program_id(0).to(tl.int64) - pid_d = tl.program_id(1).to(tl.int64) - - offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) - offs_d = pid_d * BLOCK_D + tl.arange(0, BLOCK_D) - mm_m = offs_m < M - mm_d = offs_d < D - - b = offs_m // STRIDE_B - j = offs_m % N_COL - mask_row = b * N_COL + j - mask_ptrs = mask_ptr + mask_row[:, None] * D + offs_d[None, :] - - do = tl.load( - dout_ptr + offs_m[:, None] * D + offs_d[None, :], - mask=mm_m[:, None] & mm_d[None, :], - other=0.0, - ) - mk = tl.load(mask_ptrs, mask=mm_m[:, None] & mm_d[None, :], other=0.0) - tl.store( - ddelta_ptr + offs_m[:, None] * D + offs_d[None, :], - (do * mk).to(ddelta_ptr.type.element_ty), - mask=mm_m[:, None] & mm_d[None, :], - ) - - -def _fused_dropout_residual_fwd( - pair_2d: torch.Tensor, - delta_2d: torch.Tensor, - mask_2d: torch.Tensor, - n_row: int, - n_col: int, -) -> torch.Tensor: - M, D = pair_2d.shape - out = torch.empty_like(pair_2d) - BLOCK_M = 64 - BLOCK_D = min(128, _next_pow2(D)) - grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(D, BLOCK_D)) - _fused_dropout_residual_fwd_kernel[grid]( - pair_2d, - delta_2d, - mask_2d, - out, - M, - D, - n_col, - n_row * n_col, - BLOCK_M=BLOCK_M, - BLOCK_D=BLOCK_D, - num_warps=4, # pyright: ignore[reportCallIssue] - ) - return out - - -def _fused_dropout_residual_bwd( - dout_2d: torch.Tensor, mask_2d: torch.Tensor, n_row: int, n_col: int -) -> torch.Tensor: - M, D = dout_2d.shape - ddelta = torch.empty_like(dout_2d) - BLOCK_M = 64 - BLOCK_D = min(128, _next_pow2(D)) - grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(D, BLOCK_D)) - _fused_dropout_residual_bwd_kernel[grid]( - dout_2d, - mask_2d, - ddelta, - M, - D, - n_col, - n_row * n_col, - BLOCK_M=BLOCK_M, - BLOCK_D=BLOCK_D, - num_warps=4, # pyright: ignore[reportCallIssue] - ) - return ddelta - - -def _next_pow2(n: int) -> int: - p = 1 - while p < n: - p <<= 1 - return p - - -class FusedDropoutResidualFn(torch.autograd.Function): - """`pair_out = pair + delta * mask` fused into one kernel. - - Args: - pair: [B, N_row, N_col, D] - delta: [B, N_row, N_col, D] - mask: [B, 1, N_col, D] or [1, 1, N_col, D] — already scaled to 1/(1-r) for kept entries - - The mask is row-shared (size 1 along `N_row`) — we read it directly with - modular indexing rather than broadcasting and materializing a full [B, N, N, D] - copy. - """ - - @staticmethod - def forward(ctx, pair, delta, mask): - in_shape = pair.shape # [B, N_row, N_col, D] - N_row = in_shape[-3] - N_col = in_shape[-2] - D = in_shape[-1] - pair_2d = pair.contiguous().view(-1, D) - delta_2d = delta.contiguous().view(-1, D) - # [B, 1, N_col, D] → [B*N_col, D]; kernel indexes b*N_col + (m % N_col) - # so each batch sample uses its own draw. - mask_2d = mask.contiguous().view(-1, D) - out_2d = _fused_dropout_residual_fwd(pair_2d, delta_2d, mask_2d, N_row, N_col) - ctx.save_for_backward(mask_2d) - ctx.in_shape = in_shape - ctx.n_row = N_row - ctx.n_col = N_col - return out_2d.view(in_shape) - - @staticmethod - def backward(ctx, dout): - (mask_2d,) = ctx.saved_tensors - D = ctx.in_shape[-1] - dout_2d = dout.contiguous().view(-1, D) - ddelta_2d = _fused_dropout_residual_bwd(dout_2d, mask_2d, ctx.n_row, ctx.n_col) - # Residual: gradient passes through. Mask: not differentiable. - return dout, ddelta_2d.view(ctx.in_shape), None - - -class FusedDropoutResidual(nn.Module): - """Fused module for the pattern `pair + Dropout(r, batch_dim=1)(delta)`. - - The kernel only supports row-shared dropout — i.e. a `[B, 1, N_col, D]` - mask broadcast over the row axis (dim 1) — so this module hardcodes that - layout. The `j = m % N_col` indexing in the Triton kernel would read out - of bounds with any other sharing pattern, so we don't expose `batch_dim` - as a parameter. Use plain `Dropout(r, batch_dim=2)` for col-shared. - - Usage in a pairformer block: - - # Before - pair = pair + self.row_drop(self.tri_mul_out(pair, mask=...)) - - # After - pair = self.row_drop(pair, self.tri_mul_out(pair, mask=...)) - - where `self.row_drop` is `FusedDropoutResidual(r)`. - """ - - def __init__(self, r: float): - super().__init__() - self.r = r - - def forward(self, pair: torch.Tensor, delta: torch.Tensor) -> torch.Tensor: - if not self.training or self.r == 0.0: - return pair + delta - # Use delta.dtype: F.dropout's cuRAND draw depends on input dtype, so - # building from pair (fp32) vs delta (bf16 under autocast) breaks - # same-seed parity with the unfused `pair + Dropout(delta)` path. - shape = list(pair.shape) - shape[1] = 1 # row-shared mask: [B, 1, N_col, D] - ones = delta.new_ones(shape) - mask = torch.nn.functional.dropout(ones, p=self.r, training=True) - return FusedDropoutResidualFn.apply(pair, delta, mask) # type: ignore[return-value] diff --git a/src/transformers/models/esmfold2/kernels/fused_dual_gemm.py b/src/transformers/models/esmfold2/kernels/fused_dual_gemm.py deleted file mode 100644 index 66ae88ec4208..000000000000 --- a/src/transformers/models/esmfold2/kernels/fused_dual_gemm.py +++ /dev/null @@ -1,610 +0,0 @@ -"""Native Triton ``fused_sigmoid_gated_dual_gemm`` for TriMul stage 2. - -Computes ``sigmoid(x @ w1.T) * (x @ w2.T)`` with optional row-shared mask, -bias, and transposed output. Forward + backward implemented in bf16. - -Inspired by cuequivariance's ``fused_sigmoid_gated_dual_gemm`` -(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently -re-implemented in Triton. -""" - -# ruff: noqa: E402 - -from __future__ import annotations - -import os -import warnings - -warnings.filterwarnings("ignore", category=FutureWarning) -warnings.filterwarnings("ignore", category=UserWarning) -warnings.filterwarnings("ignore", category=DeprecationWarning) -os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1") -os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1") - -import torch -import triton -import triton.language as tl - -# Static config — runtime autotune cold-start is unshippable for inference. -_AUTOTUNE_CONFIGS = [ - triton.Config( - {"TILE_M": 128, "TILE_N": 64, "TILE_K": 32, "GROUP_M": 8}, - num_stages=4, - num_warps=4, - ) -] - - -@triton.autotune( - configs=_AUTOTUNE_CONFIGS, key=["M", "N", "K", "HAS_MASK", "TRANSPOSE_OUT"] -) -@triton.jit -def _gated_dual_gemm_kernel( - x_ptr, # [M, K] bf16 - w1_ptr, # [N, K] bf16 — gate weight - w2_ptr, # [N, K] bf16 — value weight - mask_ptr, # [M] bf16 — row-shared mask broadcast to (M, N) - out_ptr, # [M, N] or [N, M] bf16 — sigmoid(x@w1) * (x@w2) - M, - N, - K, - TILE_M: tl.constexpr, - TILE_N: tl.constexpr, - TILE_K: tl.constexpr, - GROUP_M: tl.constexpr, - HAS_MASK: tl.constexpr, - TRANSPOSE_OUT: tl.constexpr, # store (N, M) instead of (M, N) - NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] -): - """Per (TILE_M, TILE_N) output tile: - gate_acc = Σ_K (x[:, k] @ w1[:, k]) over k - val_acc = Σ_K (x[:, k] @ w2[:, k]) over k - delta = sigmoid(gate_acc) * val_acc - if mask: delta *= mask[m_tile] (broadcast over N) - store delta (transposed if TRANSPOSE_OUT) - """ - pid_m_raw = tl.program_id(0) - pid_n_raw = tl.program_id(1) - - # GROUP_M swizzle for L2 reuse of ``x`` across consecutive CTAs. - num_pid_m = tl.cdiv(M, TILE_M) - num_pid_n = tl.cdiv(N, TILE_N) - pid = pid_n_raw * num_pid_m + pid_m_raw # row-major program id - num_pid_in_group = GROUP_M * num_pid_n - group_id = pid // num_pid_in_group - first_pid_m = group_id * GROUP_M - group_size_m = min(num_pid_m - first_pid_m, GROUP_M) - pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) - pid_n = (pid % num_pid_in_group) // group_size_m - - if NEEDS_INT64: - pid_m = tl.cast(pid_m, tl.int64) - pid_n = tl.cast(pid_n, tl.int64) - M = tl.cast(M, tl.int64) - N = tl.cast(N, tl.int64) - K = tl.cast(K, tl.int64) - - start_m = pid_m * TILE_M - start_n = pid_n * TILE_N - - offs_m = start_m + tl.arange(0, TILE_M) - offs_n = start_n + tl.arange(0, TILE_N) - offs_k = tl.arange(0, TILE_K) - if NEEDS_INT64: - offs_m = tl.cast(offs_m, tl.int64) - offs_n = tl.cast(offs_n, tl.int64) - offs_k = tl.cast(offs_k, tl.int64) - - x_ptrs = x_ptr + (offs_m[:, None] * K + offs_k[None, :]) - w1_base = w1_ptr + (offs_n[None, :] * K + offs_k[:, None]) - w2_base = w2_ptr + (offs_n[None, :] * K + offs_k[:, None]) - - gate_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - val_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - mask_m = offs_m < M - - for _ in range(0, tl.cdiv(K, TILE_K)): - x_raw = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0) - x_op = x_raw.to(w1_ptr.type.element_ty) - w1_tile = tl.load(w1_base) - w2_tile = tl.load(w2_base) - gate_acc = tl.dot(x_op, w1_tile, gate_acc) - val_acc = tl.dot(x_op, w2_tile, val_acc) - x_ptrs += TILE_K - w1_base += TILE_K - w2_base += TILE_K - - delta = tl.sigmoid(gate_acc) * val_acc # fp32 - - if HAS_MASK: - mask_tile = tl.load(mask_ptr + offs_m, mask=mask_m, other=0.0).to(tl.float32) - delta = delta * mask_tile[:, None] - - if TRANSPOSE_OUT: - out_ptrs = out_ptr + (offs_n[:, None] * M + offs_m[None, :]) - tl.store( - out_ptrs, tl.trans(delta).to(out_ptr.type.element_ty), mask=mask_m[None, :] - ) - else: - out_ptrs = out_ptr + (offs_m[:, None] * N + offs_n[None, :]) - tl.store(out_ptrs, delta.to(out_ptr.type.element_ty), mask=mask_m[:, None]) - - -# Backward emits per-element grad_gate_logits and grad_val_acc by recomputing -# the two forward GEMMs. Weight grads (d_w1, d_w2, d_x) are done in cuBLAS in -# the autograd Function — cuBLAS beats Triton bf16 at these reduction shapes. -# Narrower TILE_N=64 vs forward's 128 (bwd writes two M*N tensors, doubling -# register pressure). -_BWD_AUTOTUNE_CONFIGS = [ - triton.Config( - {"TILE_M": 64, "TILE_N": 64, "TILE_K": 64, "GROUP_M": 8}, - num_stages=3, - num_warps=4, - ) -] - - -@triton.autotune( - configs=_BWD_AUTOTUNE_CONFIGS, - key=["M", "N", "K", "HAS_MASK", "GRAD_OUT_TRANSPOSED", "GRAD_OUT_SPLIT"], -) -@triton.jit -def _gated_dual_gemm_backward_kernel( - grad_out_ptr, # [M, N] (or [N, M] if GRAD_OUT_TRANSPOSED, or [N/2, M] if GRAD_OUT_SPLIT) - grad_out2_ptr, # GRAD_OUT_SPLIT only: second half of (N, M); else dummy - x_ptr, # [M, K] bf16 — saved input - w1_ptr, # [N, K] bf16 - w2_ptr, # [N, K] bf16 - mask_ptr, # [M] bf16 — row-shared (unused if HAS_MASK=0) - grad_gate_logits_ptr, # [M, N] bf16 — out: d (x @ w1.T) - grad_val_acc_ptr, # [M, N] bf16 — out: d (x @ w2.T) - grad_mask_partials_ptr, # [num_pid_n, M] fp32 — partial mask grads - M, - N, - K, - HALF_N: tl.constexpr, # = N // 2, only used when GRAD_OUT_SPLIT=1 - TILE_M: tl.constexpr, - TILE_N: tl.constexpr, - TILE_K: tl.constexpr, - GROUP_M: tl.constexpr, - HAS_MASK: tl.constexpr, - GRAD_OUT_TRANSPOSED: tl.constexpr, # 1: load grad from (N, M) layout - GRAD_OUT_SPLIT: tl.constexpr, # 1: read from two (N/2, M) tensors (chunk-free path) - NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] -): - """Per (TILE_M, TILE_N) output tile: - gate_acc = Σ_K x[:, k] @ w1[:, k] - val_acc = Σ_K x[:, k] @ w2[:, k] - g = sigmoid(gate_acc) - grad_o = grad_out_tile (post-mask: multiply by mask if HAS_MASK) - d_gate_logits = grad_o * val_acc * g * (1 - g) - d_val_acc = grad_o * g - d_mask_partial[pid_n, m] = sum_n (grad_out_tile * g * val_acc) [if HAS_MASK] - """ - pid_m_raw = tl.program_id(axis=0) - pid_n_raw = tl.program_id(axis=1) - - num_pid_m = tl.cdiv(M, TILE_M) - num_pid_n = tl.cdiv(N, TILE_N) - pid = pid_n_raw * num_pid_m + pid_m_raw - num_pid_in_group = GROUP_M * num_pid_n - group_id = pid // num_pid_in_group - first_pid_m = group_id * GROUP_M - group_size_m = min(num_pid_m - first_pid_m, GROUP_M) - pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) - pid_n = (pid % num_pid_in_group) // group_size_m - - if NEEDS_INT64: - pid_m = tl.cast(pid_m, tl.int64) - pid_n = tl.cast(pid_n, tl.int64) - M = tl.cast(M, tl.int64) - N = tl.cast(N, tl.int64) - K = tl.cast(K, tl.int64) - - start_m = pid_m * TILE_M - start_n = pid_n * TILE_N - - offs_m = start_m + tl.arange(0, TILE_M) - offs_n = start_n + tl.arange(0, TILE_N) - offs_k = tl.arange(0, TILE_K) - if NEEDS_INT64: - offs_m = tl.cast(offs_m, tl.int64) - offs_n = tl.cast(offs_n, tl.int64) - offs_k = tl.cast(offs_k, tl.int64) - - x_ptrs = x_ptr + (offs_m[:, None] * K + offs_k[None, :]) - w1_base = w1_ptr + (offs_n[None, :] * K + offs_k[:, None]) - w2_base = w2_ptr + (offs_n[None, :] * K + offs_k[:, None]) - - gate_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - val_acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - mask_m = offs_m < M - - # Recompute fwd GEMMs (cheaper than saving N*M activations). - for _ in range(0, tl.cdiv(K, TILE_K)): - x_tile = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0) - x_op = x_tile.to(w1_ptr.type.element_ty) - w1_tile = tl.load(w1_base) - w2_tile = tl.load(w2_base) - gate_acc = tl.dot(x_op, w1_tile, gate_acc) - val_acc = tl.dot(x_op, w2_tile, val_acc) - x_ptrs += TILE_K - w1_base += TILE_K - w2_base += TILE_K - - g = tl.sigmoid(gate_acc) - - if GRAD_OUT_SPLIT: - # Two (N/2, M) grad tensors; caller guarantees TILE_N divides HALF_N. - if start_n < HALF_N: - offs_n_local = offs_n - grad_o_ptrs = grad_out_ptr + (offs_n_local[None, :] * M + offs_m[:, None]) - else: - offs_n_local = offs_n - HALF_N - grad_o_ptrs = grad_out2_ptr + (offs_n_local[None, :] * M + offs_m[:, None]) - elif GRAD_OUT_TRANSPOSED: - grad_o_ptrs = grad_out_ptr + (offs_n[None, :] * M + offs_m[:, None]) - else: - grad_o_ptrs = grad_out_ptr + (offs_m[:, None] * N + offs_n[None, :]) - grad_o = tl.load(grad_o_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - - if HAS_MASK: - # d_mask = row-sum BEFORE mask multiply; per-pid_n partial, host reduces. - d_mask_partial = tl.sum(grad_o * g * val_acc, axis=1) - d_mask_partials_ptrs = grad_mask_partials_ptr + pid_n * M + offs_m - tl.store(d_mask_partials_ptrs, d_mask_partial, mask=mask_m) - mask_tile = tl.load(mask_ptr + offs_m, mask=mask_m, other=0.0).to(tl.float32) - grad_o = grad_o * mask_tile[:, None] - - # Both grad ptrs index into one (M, 2N) buffer (val_acc cols [0:N], - # gate_logits cols [N:2N]) — lets downstream d_x / d_w fold into single GEMMs. - d_val_acc = (grad_o * g).to(grad_val_acc_ptr.type.element_ty) - d_val_acc_ptrs = grad_val_acc_ptr + (offs_m[:, None] * (2 * N) + offs_n[None, :]) - tl.store(d_val_acc_ptrs, d_val_acc, mask=mask_m[:, None]) - - d_gate_logits = (grad_o * val_acc * g * (1.0 - g)).to( - grad_gate_logits_ptr.type.element_ty - ) - d_gate_logits_ptrs = grad_gate_logits_ptr + ( - offs_m[:, None] * (2 * N) + offs_n[None, :] - ) - tl.store(d_gate_logits_ptrs, d_gate_logits, mask=mask_m[:, None]) - - -# Backward TILE_N must match the kernel's autotuned tile in ``_BWD_AUTOTUNE_CONFIGS``; -# host code uses it to size the per-tile mask-grad partials buffer statically. -_BWD_TILE_N = 64 - - -def _fused_gated_dual_gemm_bwd( - grad_out: torch.Tensor, # (M, N) or (N, M) layout (see grad_out_transposed) - x: torch.Tensor, # [..., K] (un-flattened, original) - w1: torch.Tensor, - w2: torch.Tensor, - mask: torch.Tensor | None, - grad_out_transposed: bool = False, - grad_out_split: tuple[torch.Tensor, torch.Tensor] | None = None, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]: - """Compute (d_x, d_w1, d_w2, d_mask) for ``fused_gated_dual_gemm``. - - Three grad_out layouts supported: - * default: ``grad_out`` of shape ``(..., N)`` flattened to (M, N). - * ``grad_out_transposed=True``: shape ``(N, ...)`` flattened to (N, M). - * ``grad_out_split=(g1, g2)``: two tensors, each of shape ``(N/2, ...)`` - flattened to ``(N/2, M)``; the kernel reads from the appropriate - pointer per tile. Avoids the autograd-introduced concat that - ``torch.chunk(dim=0)``'s backward inserts (saves 3-4 ms at prod - shape, B=5 L=768 c_z=128). - """ - in_shape = x.shape - K = in_shape[-1] - x_c = x if x.is_contiguous() else x.contiguous() - x_2d = x_c.view(-1, K) - M = x_2d.shape[0] - N = w1.shape[0] - - assert ( - w1.dtype == w2.dtype == torch.bfloat16 - ), f"weights must be bf16; got {w1.dtype}/{w2.dtype}" - assert x_2d.dtype == torch.bfloat16, "bwd only supports bf16 x" - - if grad_out_split is not None: - g1, g2 = grad_out_split - half_n = N // 2 - # Materialize only if non-contig (avoids a full-tensor copy). - if not g1.is_contiguous(): - g1 = g1.contiguous() - if not g2.is_contiguous(): - g2 = g2.contiguous() - assert g1.numel() == half_n * M and g2.numel() == half_n * M, ( - f"split grad sizes mismatch: g1={g1.numel()} g2={g2.numel()} " - f"vs half_n*M={half_n * M}" - ) - grad_out_2d_a = g1.view(half_n, M) - grad_out_2d_b = g2.view(half_n, M) - elif grad_out_transposed: - grad_out_2d_a = grad_out.contiguous().view(N, M) - grad_out_2d_b = grad_out_2d_a # unused (dummy) - else: - grad_out_2d_a = grad_out.contiguous().view(M, N) - grad_out_2d_b = grad_out_2d_a # unused (dummy) - - mask_flat = mask.contiguous().view(-1) if mask is not None else None - if mask_flat is not None: - assert mask_flat.shape[0] == M - - # (M, 2N) combined-grad buffer; matched stacked_w order (w2 then w1) - # lets d_w and d_x each collapse to a single cuBLAS GEMM. - grad_combined = torch.empty((M, 2 * N), device=x.device, dtype=torch.bfloat16) - grad_val_acc = grad_combined[:, :N] - grad_gate_logits = grad_combined[:, N:] - - tiles_n_max = triton.cdiv(N, _BWD_TILE_N) - if mask is not None: - grad_mask_partials = torch.empty( - (tiles_n_max, M), device=x.device, dtype=torch.float32 - ) - else: - grad_mask_partials = torch.empty((1,), device=x.device, dtype=torch.float32) - - _dummy_mask = torch.zeros((), device=x.device, dtype=torch.bfloat16) - - # (M, 2N) layout → use stride 2N for the int32-overflow gate. - NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * (2 * N) >= 2**31 - 1) - - def grid(meta): - assert N % meta["TILE_N"] == 0 - return (triton.cdiv(M, meta["TILE_M"]), N // meta["TILE_N"]) - - half_n = N // 2 if grad_out_split is not None else 1 - - _gated_dual_gemm_backward_kernel[grid]( - grad_out_2d_a, - grad_out_2d_b, - x_2d, - w1.contiguous(), - w2.contiguous(), - mask_flat if mask_flat is not None else _dummy_mask, - grad_gate_logits, - grad_val_acc, - grad_mask_partials, - M, - N, - K, - HALF_N=half_n, - HAS_MASK=mask is not None, - GRAD_OUT_TRANSPOSED=grad_out_transposed, - GRAD_OUT_SPLIT=grad_out_split is not None, - NEEDS_INT64=NEEDS_INT64, - ) - - # Stacked weights for the single-GEMM d_x path. - stacked_w = torch.cat([w2, w1], dim=0) # (2N, K), matches val|gate layout - - d_x_2d = grad_combined @ stacked_w - d_x = d_x_2d.view(in_shape) - - d_w_combined = grad_combined.t() @ x_2d # (2N, K) - d_w2 = d_w_combined[:N] - d_w1 = d_w_combined[N:] - - if mask is not None: - actual_tiles = triton.cdiv(N, _BWD_TILE_N) - d_mask_flat = grad_mask_partials[:actual_tiles].sum(dim=0).to(mask.dtype) - d_mask = d_mask_flat.view(mask.shape) - else: - d_mask = None - - return d_x, d_w1, d_w2, d_mask - - -class FusedGatedDualGEMM(torch.autograd.Function): - """Autograd wrapper for ``fused_gated_dual_gemm`` (bf16 path, - ``transpose_out=False``). - - Saves ``x``, ``w1``, ``w2``, ``mask`` for the backward kernel; the - backward kernel recomputes the two dual-GEMM activations (cheap vs - materializing them in fwd context). - """ - - @staticmethod - def forward( - ctx, - x: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - mask: torch.Tensor | None, - ) -> torch.Tensor: - out = _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=False) - if mask is None: - ctx.save_for_backward(x, w1, w2) - ctx.has_mask = False - else: - ctx.save_for_backward(x, w1, w2, mask) - ctx.has_mask = True - return out - - @staticmethod - def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] - if ctx.has_mask: - x, w1, w2, mask = ctx.saved_tensors - else: - x, w1, w2 = ctx.saved_tensors - mask = None - if not grad_out.is_contiguous(): - grad_out = grad_out.contiguous() - d_x, d_w1, d_w2, d_mask = _fused_gated_dual_gemm_bwd(grad_out, x, w1, w2, mask) - return d_x, d_w1, d_w2, d_mask - - -class FusedGatedDualGEMMSplit(torch.autograd.Function): - """Autograd wrapper that returns the dual-GEMM output as two split halves. - - Forward: runs the kernel with ``transpose_out=True``, producing a single - ``(N=2*c_z, *trailing)`` buffer where the gate half (first c_z) and the - value half (next c_z) live contiguously along dim 0. Returns the two - views ``(a, b_t)`` so downstream einsums consume them in - ``(c_z, B, L, L)`` layout — no chunk in the autograd graph. - - Backward: receives ``(grad_a, grad_b_t)`` (each ``(c_z, *trailing)``) - and passes them as the ``grad_out_split`` pair to the backward kernel. - Eliminates the ~3.5 ms ``torch.chunk(dim=0)`` backward concat at the - prod shape (B=5, L=768, c_z=128) by reading the two halves from - separate pointers inside the kernel. - """ - - @staticmethod - def forward( - ctx, - x: torch.Tensor, # (B, L, L, c_z) bf16 - w1: torch.Tensor, # (N=2*c_z, c_z) bf16 — gate - w2: torch.Tensor, # (N=2*c_z, c_z) bf16 — value - mask: torch.Tensor | None, # (B, L, L) bf16 or None - trailing_shape: tuple, # (B, L, L) for output view - ) -> tuple[torch.Tensor, torch.Tensor]: - out = _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=True) - # Slicing on a leading dim of a contiguous tensor stays contiguous (no copy). - N = w1.shape[0] - half_n = N // 2 - out_view = out.view((N,) + trailing_shape) - a = out_view[:half_n] - b_t = out_view[half_n:] - if mask is None: - ctx.save_for_backward(x, w1, w2) - ctx.has_mask = False - else: - ctx.save_for_backward(x, w1, w2, mask) - ctx.has_mask = True - return a, b_t - - @staticmethod - def backward(ctx, grad_a: torch.Tensor, grad_b_t: torch.Tensor): # type: ignore[override] - if ctx.has_mask: - x, w1, w2, mask = ctx.saved_tensors - else: - x, w1, w2 = ctx.saved_tensors - mask = None - # Don't call .contiguous() — _fused_gated_dual_gemm_bwd validates instead - # (avoids a full-tensor copy on the common contig path). - d_x, d_w1, d_w2, d_mask = _fused_gated_dual_gemm_bwd( - None, # type: ignore[arg-type] - x, - w1, - w2, - mask, - grad_out_split=(grad_a, grad_b_t), - ) - return d_x, d_w1, d_w2, d_mask, None - - -def fused_gated_dual_gemm_split( - x: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - mask: torch.Tensor | None = None, -) -> tuple[torch.Tensor, torch.Tensor]: - """Split-output variant of ``fused_gated_dual_gemm``: returns ``(a, b_t)`` - with layout ``(c_z, B, L, L)`` each, avoiding chunk in the autograd graph. - - Inference fallback (no grad) just calls the regular fwd with - ``transpose_out=True`` and chunks the result — same layout, no kernel - change required. - """ - trailing_shape = tuple(x.shape[:-1]) - if torch.is_grad_enabled() and ( - x.requires_grad or w1.requires_grad or w2.requires_grad - ): - return FusedGatedDualGEMMSplit.apply(x, w1, w2, mask, trailing_shape) # type: ignore[return-value] - out = _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=True) - N = w1.shape[0] - out_view = out.view((N,) + trailing_shape) - half_n = N // 2 - return out_view[:half_n], out_view[half_n:] - - -def _fused_gated_dual_gemm_fwd( - x: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - mask: torch.Tensor | None = None, - transpose_out: bool = False, -) -> torch.Tensor: - """Native Triton implementation of sigmoid-gated dual GEMM. - - Computes ``sigmoid(x @ w1.T) * (x @ w2.T)`` with optional row-shared mask. - - Shapes: - x: (..., K) bf16 - w1: (N, K) bf16 - w2: (N, K) bf16 - mask: (...,) bf16 — flattened to a per-row scalar - out: (..., N) or (N, ...) when ``transpose_out=True`` - """ - in_shape = x.shape - K = in_shape[-1] - x_2d = x.contiguous().view(-1, K) - M = x_2d.shape[0] - N = w1.shape[0] - - assert w1.shape == w2.shape, f"w1 {w1.shape} ≠ w2 {w2.shape}" - assert w1.shape[1] == K - assert ( - w1.dtype == w2.dtype == torch.bfloat16 - ), f"weights must be bf16; got {w1.dtype}/{w2.dtype}" - - out_dtype = torch.bfloat16 - if transpose_out: - out = torch.empty((N, M), device=x.device, dtype=out_dtype) - out_shape = (N,) + in_shape[:-1] - else: - out = torch.empty((M, N), device=x.device, dtype=out_dtype) - out_shape = in_shape[:-1] + (N,) - - mask_flat = mask.contiguous().view(-1) if mask is not None else None - if mask_flat is not None: - assert mask_flat.shape[0] == M, f"mask len {mask_flat.shape[0]} ≠ M {M}" - - _dummy = torch.zeros((), device=x.device, dtype=torch.float32) - - NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * N >= 2**31 - 1) - - def grid(meta): - assert N % meta["TILE_N"] == 0 - return (triton.cdiv(M, meta["TILE_M"]), N // meta["TILE_N"]) - - _gated_dual_gemm_kernel[grid]( - x_2d, - w1.contiguous(), - w2.contiguous(), - mask_flat if mask_flat is not None else _dummy, - out, - M, - N, - K, - HAS_MASK=mask is not None, - TRANSPOSE_OUT=transpose_out, - NEEDS_INT64=NEEDS_INT64, - ) - return out.view(out_shape) - - -def fused_gated_dual_gemm( - x: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - mask: torch.Tensor | None = None, - transpose_out: bool = False, -) -> torch.Tensor: - """Public wrapper: dispatches to autograd Function if grad is enabled. - - ``transpose_out=True`` is inference-only (bypasses autograd). - """ - if torch.is_grad_enabled() and ( - x.requires_grad or w1.requires_grad or w2.requires_grad - ): - assert not transpose_out, ( - "transpose_out=True is inference-only; train path must use the " - "non-transposed output (post-stage-3 einsum already handles layout)" - ) - return FusedGatedDualGEMM.apply(x, w1, w2, mask) # type: ignore[return-value] - return _fused_gated_dual_gemm_fwd(x, w1, w2, mask=mask, transpose_out=transpose_out) diff --git a/src/transformers/models/esmfold2/kernels/fused_ln_residual.py b/src/transformers/models/esmfold2/kernels/fused_ln_residual.py deleted file mode 100644 index ec349e64a830..000000000000 --- a/src/transformers/models/esmfold2/kernels/fused_ln_residual.py +++ /dev/null @@ -1,397 +0,0 @@ -"""Triton LayerNorm (bf16 IO, fp32 stats) with optional fused residual-add in -the *backward* pass. - -Inspired by cuequivariance's ``layer_norm_transpose`` -(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently -re-implemented in Triton to add the bwd residual-link fusion. - -Used by ``trimul_with_residual.triangle_multiplicative_update_with_residual`` -for two LN calls: - - * Stage 1 LN: ``x_in = LN(pair)`` with layout ``bijd->bijd``. The downstream - residual add ``out = residual + delta`` (where ``residual = pair``) flows - ``grad_out`` straight back to ``grad_pair``. Fusing that add into LN's - bwd kernel saves one full pair-tensor read + one full write (~250 MB at - B=5 L=768 c_z=128 bf16). Exposed via :func:`fused_ln_with_residual_link`. - - * Stage 4 LN: ``x_out = LN(einsum(...))`` with layout ``dbij->bijd``. No - residual to fuse — :func:`fused_ln_transpose` is the plain variant. - -The residual-link fusion in the bwd pass is the motivation for shipping this: -the bwd kernel computes ``grad_x = LN_bwd(grad_y, x, w, mean, rstd)`` AND -optionally adds an external ``grad_residual`` tensor into ``grad_x`` in the -same pass — saves one HBM round-trip of the (M, D) tensor. - -Forward + backward use a single static ``triton.Config`` each (runtime -autotune cold-start is unshippable for inference paths). The bwd pass -emits per-tile ``grad_w``/``grad_b`` fp32 partials reduced host-side; this -is the standard Triton LN-bwd idiom (see openai/triton tutorial 05). -""" - -import torch -import triton -import triton.language as tl - -# Layout enum: 0 == "bijd->bijd" (bnd contig), 1 == "dbij->bijd" (dbn contig in). -_LAYOUT_BND_BND = 0 -_LAYOUT_DBN_BND = 1 - -_FWD_TILE_M = 64 -_FWD_NUM_WARPS = 8 -_FWD_NUM_STAGES = 2 - -_BWD_TILE_M = 64 -_BWD_NUM_WARPS = 8 -_BWD_NUM_STAGES = 2 - - -@triton.jit -def _ln_fwd_kernel( - x_ptr, # input - w_ptr, # [D] - b_ptr, # [D] - out_ptr, # [M, D] contig output - mean_ptr, # [M] fp32 - rstd_ptr, # [M] fp32 - M, - D: tl.constexpr, - EPS: tl.constexpr, - LAYOUT: tl.constexpr, - TILE_M: tl.constexpr, -): - pid = tl.program_id(axis=0).to(tl.int64) - M64 = M.to(tl.int64) - - offs_m = pid * TILE_M + tl.arange(0, TILE_M).to(tl.int64) - offs_d = tl.arange(0, D).to(tl.int64) - mask_m = offs_m < M64 - - if LAYOUT == 0: - x_ptrs = x_ptr + offs_m[:, None] * D + offs_d[None, :] - x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - else: # LAYOUT == 1: (D, M) contig - x_ptrs = x_ptr + offs_d[None, :] * M64 + offs_m[:, None] - x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - - mean = tl.sum(x, axis=1) / D - x_c = x - mean[:, None] - var = tl.sum(x_c * x_c, axis=1) / D - rstd = 1.0 / tl.sqrt(var + EPS) - x_hat = x_c * rstd[:, None] - - tl.store(mean_ptr + offs_m, mean, mask=mask_m) - tl.store(rstd_ptr + offs_m, rstd, mask=mask_m) - - w = tl.load(w_ptr + offs_d).to(tl.float32) - b = tl.load(b_ptr + offs_d).to(tl.float32) - y = x_hat * w[None, :] + b[None, :] - - out_ptrs = out_ptr + offs_m[:, None] * D + offs_d[None, :] - tl.store(out_ptrs, y.to(out_ptr.type.element_ty), mask=mask_m[:, None]) - - -@triton.jit -def _ln_bwd_kernel( - grad_y_ptr, # [M, D] (always bnd; LN out is bnd) - x_ptr, # input — layout LAYOUT - w_ptr, # [D] - mean_ptr, # [M] fp32 - rstd_ptr, # [M] fp32 - grad_x_ptr, # output — layout LAYOUT (same as x) - grad_w_partial_ptr, # [num_tiles, D] fp32 - grad_b_partial_ptr, # [num_tiles, D] fp32 - grad_residual_ptr, # [M, D] in bnd layout; ignored if HAS_RESIDUAL=0 - M, - D: tl.constexpr, - LAYOUT: tl.constexpr, - HAS_RESIDUAL: tl.constexpr, - TILE_M: tl.constexpr, -): - pid = tl.program_id(axis=0).to(tl.int64) - M64 = M.to(tl.int64) - - offs_m = pid * TILE_M + tl.arange(0, TILE_M).to(tl.int64) - offs_d = tl.arange(0, D).to(tl.int64) - mask_m = offs_m < M64 - - grad_y_ptrs = grad_y_ptr + offs_m[:, None] * D + offs_d[None, :] - grad_y = tl.load(grad_y_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - - if LAYOUT == 0: - x_ptrs = x_ptr + offs_m[:, None] * D + offs_d[None, :] - x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - else: - x_ptrs = x_ptr + offs_d[None, :] * M64 + offs_m[:, None] - x = tl.load(x_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - - mean = tl.load(mean_ptr + offs_m, mask=mask_m, other=0.0) - rstd = tl.load(rstd_ptr + offs_m, mask=mask_m, other=0.0) - w = tl.load(w_ptr + offs_d).to(tl.float32) - - x_hat = (x - mean[:, None]) * rstd[:, None] - wdy = grad_y * w[None, :] - c1 = tl.sum(wdy, axis=1) / D - c2 = tl.sum(wdy * x_hat, axis=1) / D - grad_x = (wdy - (c1[:, None] + x_hat * c2[:, None])) * rstd[:, None] - - if HAS_RESIDUAL: - gr_ptrs = grad_residual_ptr + offs_m[:, None] * D + offs_d[None, :] - gr = tl.load(gr_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - grad_x = grad_x + gr - - if LAYOUT == 0: - gx_ptrs = grad_x_ptr + offs_m[:, None] * D + offs_d[None, :] - tl.store(gx_ptrs, grad_x.to(grad_x_ptr.type.element_ty), mask=mask_m[:, None]) - else: - gx_ptrs = grad_x_ptr + offs_d[None, :] * M64 + offs_m[:, None] - tl.store(gx_ptrs, grad_x.to(grad_x_ptr.type.element_ty), mask=mask_m[:, None]) - - # Per-tile partial reduction → write to (num_tiles, D) fp32 buffer. - # Final reduction (sum over num_tiles) happens host-side as a torch.sum - # (standard Triton LN-bwd idiom; see openai/triton tutorial 05). - mask_f = mask_m[:, None].to(tl.float32) - dw_tile = tl.sum(grad_y * x_hat * mask_f, axis=0) - db_tile = tl.sum(grad_y * mask_f, axis=0) - dw_offs = pid * D + offs_d - db_offs = pid * D + offs_d - tl.store(grad_w_partial_ptr + dw_offs, dw_tile) - tl.store(grad_b_partial_ptr + db_offs, db_tile) - - -def _layout_to_int(layout: str) -> int: - if layout in ("bijd->bijd", "bnd->bnd"): - return _LAYOUT_BND_BND - if layout in ("dbij->bijd", "dbn->bnd"): - return _LAYOUT_DBN_BND - raise ValueError(f"unsupported layout {layout!r}") - - -def _reshape_for_layout( - x: torch.Tensor, layout: str -) -> tuple[tuple[int, ...], int, int, torch.Tensor]: - """Reshape x to 2D LN view. Returns (out_shape, M, D, x_view).""" - if layout == "bijd->bijd": - B, II, J, D = x.shape - M = B * II * J - return (B, II, J, D), M, D, x.contiguous().view(M, D) - if layout == "bnd->bnd": - B, N, D = x.shape - M = B * N - return (B, N, D), M, D, x.contiguous().view(M, D) - if layout == "dbij->bijd": - D, B, II, J = x.shape - M = B * II * J - return (B, II, J, D), M, D, x.contiguous().view(D, M) - if layout == "dbn->bnd": - D, B, N = x.shape - M = B * N - return (B, N, D), M, D, x.contiguous().view(D, M) - raise ValueError(f"unsupported layout {layout!r}") - - -def _ln_fwd( - x_view: torch.Tensor, - w: torch.Tensor, - b: torch.Tensor, - eps: float, - layout_int: int, - M: int, - D: int, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - out = torch.empty((M, D), device=x_view.device, dtype=x_view.dtype) - mean = torch.empty((M,), device=x_view.device, dtype=torch.float32) - rstd = torch.empty((M,), device=x_view.device, dtype=torch.float32) - grid = (triton.cdiv(M, _FWD_TILE_M),) - _ln_fwd_kernel[grid]( - x_view, - w, - b, - out, - mean, - rstd, - M, - D=D, - EPS=eps, - LAYOUT=layout_int, - TILE_M=_FWD_TILE_M, - num_warps=_FWD_NUM_WARPS, # type: ignore[call-arg] - num_stages=_FWD_NUM_STAGES, # type: ignore[call-arg] - ) - return out, mean, rstd - - -def _ln_bwd( - grad_y: torch.Tensor, - x_view: torch.Tensor, - w: torch.Tensor, - mean: torch.Tensor, - rstd: torch.Tensor, - layout_int: int, - grad_residual: torch.Tensor | None, - M: int, - D: int, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - if layout_int == _LAYOUT_BND_BND: - grad_x = torch.empty((M, D), device=x_view.device, dtype=x_view.dtype) - else: - grad_x = torch.empty((D, M), device=x_view.device, dtype=x_view.dtype) - - num_tiles = triton.cdiv(M, _BWD_TILE_M) - grad_w_partial = torch.empty( - (num_tiles, D), device=x_view.device, dtype=torch.float32 - ) - grad_b_partial = torch.empty( - (num_tiles, D), device=x_view.device, dtype=torch.float32 - ) - - has_residual = grad_residual is not None - _dummy = torch.empty((), device=x_view.device, dtype=grad_y.dtype) - grid = (num_tiles,) - _ln_bwd_kernel[grid]( - grad_y, - x_view, - w, - mean, - rstd, - grad_x, - grad_w_partial, - grad_b_partial, - grad_residual if has_residual else _dummy, - M, - D=D, - LAYOUT=layout_int, - HAS_RESIDUAL=has_residual, - TILE_M=_BWD_TILE_M, - num_warps=_BWD_NUM_WARPS, # type: ignore[call-arg] - num_stages=_BWD_NUM_STAGES, # type: ignore[call-arg] - ) - - grad_w = grad_w_partial.sum(dim=0).to(w.dtype) - grad_b = grad_b_partial.sum(dim=0).to(w.dtype) - return grad_x, grad_w, grad_b - - -class _LayerNormTransposeFn(torch.autograd.Function): - @staticmethod - def forward( - ctx, x: torch.Tensor, w: torch.Tensor, b: torch.Tensor, eps: float, layout: str - ) -> torch.Tensor: - layout_int = _layout_to_int(layout) - out_shape, M, D, x_view = _reshape_for_layout(x, layout) - out_bnd, mean, rstd = _ln_fwd(x_view, w, b, eps, layout_int, M, D) - ctx.save_for_backward(x_view, w, mean, rstd) - ctx.layout_int = layout_int - ctx.M = M - ctx.D = D - ctx.x_orig_shape = x.shape - return out_bnd.view(*out_shape) - - @staticmethod - def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] - x_view, w, mean, rstd = ctx.saved_tensors - grad_y = grad_out.contiguous().view(ctx.M, ctx.D) - grad_x, grad_w, grad_b = _ln_bwd( - grad_y, x_view, w, mean, rstd, ctx.layout_int, None, ctx.M, ctx.D - ) - grad_x = grad_x.view(*ctx.x_orig_shape) - return grad_x, grad_w, grad_b, None, None - - -def fused_ln_transpose( - x: torch.Tensor, - w: torch.Tensor, - b: torch.Tensor, - eps: float = 1e-5, - layout: str = "bijd->bijd", -) -> torch.Tensor: - """Plain LN replacement for ``layer_norm_transpose`` (no residual fusion).""" - return _LayerNormTransposeFn.apply(x, w, b, eps, layout) # type: ignore[return-value] - - -# Stage-1 LN with residual-add folded into the bwd kernel. The Function returns -# (ln_out, residual_alias); downstream uses ln_out, stage-5 uses residual_alias -# as the residual input. In bwd we receive both grad tensors and fold -# grad_residual_alias into grad_x in-kernel (saves one HBM round-trip). -# residual_alias is a fresh tensor (not a view) so autograd reliably routes -# its grad back through this Function. - - -class _LayerNormWithResidualLinkFn(torch.autograd.Function): - @staticmethod - def forward( - ctx, - x: torch.Tensor, - w: torch.Tensor, - b: torch.Tensor, - residual_link: torch.Tensor, - eps: float, - layout: str, - ) -> tuple[torch.Tensor, torch.Tensor]: - layout_int = _layout_to_int(layout) - out_shape, M, D, x_view = _reshape_for_layout(x, layout) - out_bnd, mean, rstd = _ln_fwd(x_view, w, b, eps, layout_int, M, D) - ctx.save_for_backward(x_view, w, mean, rstd) - ctx.layout_int = layout_int - ctx.M = M - ctx.D = D - ctx.x_orig_shape = x.shape - ctx.residual_link_shape = residual_link.shape - - ln_out = out_bnd.view(*out_shape) - # residual_alias: returning ``residual_link`` itself works in - # custom Functions — autograd treats the input-as-output case - # correctly (sums grads back into the input). The downstream - # graph node ``out = residual_alias + delta`` sees a regular - # tensor and produces grad of shape == residual_link. - # Note: we use .view_as for safety so the AutogradMeta is fresh. - return ln_out, residual_link.view_as(residual_link) - - @staticmethod - def backward(ctx, grad_ln_out: torch.Tensor, grad_link_pass: torch.Tensor): # type: ignore[override] - x_view, w, mean, rstd = ctx.saved_tensors - grad_y = grad_ln_out.contiguous().view(ctx.M, ctx.D) - - # grad_link_pass shape == residual_link shape. - if grad_link_pass is None: - grad_residual = None - else: - grad_residual = grad_link_pass.contiguous().view(ctx.M, ctx.D) - - grad_x, grad_w, grad_b = _ln_bwd( - grad_y, x_view, w, mean, rstd, ctx.layout_int, grad_residual, ctx.M, ctx.D - ) - grad_x = grad_x.view(*ctx.x_orig_shape) - - # We folded grad_link_pass into grad_x → return None for that input's - # grad slot, since we've already accounted for it via x's grad path - # (the caller is expected to pass x == residual_link, so grad_x += - # grad_residual is exactly the combined grad on the shared leaf). - # If caller passes DIFFERENT tensors for x and residual_link, the - # link's grad is *lost* — that's a usage error we accept by contract. - return grad_x, grad_w, grad_b, None, None, None - - -def fused_ln_with_residual_link( - x: torch.Tensor, - w: torch.Tensor, - b: torch.Tensor, - residual_link: torch.Tensor, - eps: float = 1e-5, - layout: str = "bijd->bijd", -) -> tuple[torch.Tensor, torch.Tensor]: - """LN(x) with a residual-link passthrough that fuses - ``grad_residual_link`` into the LN backward kernel. - - Contract: ``x`` and ``residual_link`` MUST refer to the same leaf - tensor (same identity). The returned ``residual_alias`` MUST be used as - the residual input to the downstream add — otherwise the fusion routes - the grad incorrectly. - - Returns ``(ln_out, residual_alias)``. - """ - if x is not residual_link: - raise ValueError( - "fused_ln_with_residual_link requires x and residual_link to be the " - "same tensor instance (the caller must wire pair → both)." - ) - return _LayerNormWithResidualLinkFn.apply(x, w, b, residual_link, eps, layout) # type: ignore[return-value] diff --git a/src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py b/src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py deleted file mode 100644 index ff507d84fee5..000000000000 --- a/src/transformers/models/esmfold2/kernels/fused_lnlin_swiglu.py +++ /dev/null @@ -1,415 +0,0 @@ -"""Fused LayerNorm + Linear(d, 2*d_inner) + SwiGLU(silu(x1) * x2) kernel. - -Collapses the standard LayerNorm -> Linear -> chunk -> SiLU -> mul sequence -into a single Triton kernel: - - out[..., d_inner] = silu(x1) * x2 where (x1, x2) = chunk(LN(x) @ W12, 2) - -Compared to the unfused PyTorch sequence this kernel: - - 1. Eliminates the [M, 2*d_inner] HBM read between Linear and SwiGLU. - 2. Halves X reads in the matmul: each program produces both halves of the - linear output (against W12_a and W12_b) in one pass over X. - 3. Fuses LayerNorm into the matmul k-loop. - -The output ordering uses silu of the FIRST half of W12's output, so the -fused module matches the standard LN+SwiGLU MLP composition. - -Backward: SwiGLU bwd is a small Triton kernel that mutates the saved [M, 2N] -linear-output buffer in place to produce dlin (no extra alloc). LN+Linear bwd -uses cuBLAS gemm + ATen's `native_layer_norm_backward` — fast on H100 and -avoids needing per-shape autotuned Triton bwd configs. - -Tuned forward configs were autotuned on H100 80GB for the pair-transition -production shapes (d_pair=256, d_inner=1024, M=B*N*N at N=384 / 640). -""" - -from __future__ import annotations - -import math - -import torch -import torch.nn as nn -import torch.nn.functional as F -import triton -import triton.language as tl - - -@triton.jit -def _ln_stats_kernel( - X_ptr, - X_row_stride, - Mean_ptr, - Mean_row_stride, - Rstd_ptr, - Rstd_row_stride, - K, - eps, - BLOCK_SIZE: tl.constexpr, -): - """Per-row LayerNorm reduction: writes mean and 1/sqrt(var+eps) for each row of X.""" - row = tl.program_id(0).to(tl.int64) - cols = tl.arange(0, BLOCK_SIZE) - mask = cols < K - - x = tl.load(X_ptr + row * X_row_stride + cols, mask=mask, other=0.0) - mean = tl.sum(x, axis=0) / K - centered = tl.where(mask, x - mean, 0.0) - var = tl.sum(centered * centered, axis=0) / K - rstd = 1.0 / tl.sqrt(var + eps) - - tl.store(Mean_ptr + row * Mean_row_stride, mean) - tl.store(Rstd_ptr + row * Rstd_row_stride, rstd) - - -@triton.jit -def _lnlin_swiglu_fwd_kernel( - X_ptr, # [M, K] - W_ptr, # [K, 2N] — first half (cols [0:N]) feeds the silu input; - # second half (cols [N:2N]) is the gate output - LN_W_ptr, # [K] - LN_B_ptr, # [K] - Lin_ptr, # [M, 2N] — full linear output (saved for backward) - Out_ptr, # [M, N] — silu(x1) * x2 (input to the next Linear) - Mean_ptr, - Rstd_ptr, - M, - N, - K, - stride_xm, - stride_xk, - stride_wk, - stride_wn, - stride_lin_m, - stride_lin_n, - stride_out_m, - stride_out_n, - HAS_LN_BIAS: tl.constexpr, - BLOCK_SIZE_M: tl.constexpr, - BLOCK_SIZE_N: tl.constexpr, - BLOCK_SIZE_K: tl.constexpr, - GROUP_SIZE_M: tl.constexpr, -): - """One program produces a [BLOCK_M, BLOCK_N] tile of `Out`. To avoid - re-reading X for the two halves of W12, the program does TWO matmul - accumulators (against W_a and W_b) in the same K-loop, producing both - halves of the linear output for the same M tile. Then SwiGLU is applied - in registers and both the [M, 2N] linear output and [M, N] swiglu output - are written.""" - pid = tl.program_id(axis=0).to(tl.int64) - num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) - num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) - num_pid_in_group = GROUP_SIZE_M * num_pid_n - group_id = pid // num_pid_in_group - first_pid_m = group_id * GROUP_SIZE_M - group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) - pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) - pid_n = (pid % num_pid_in_group) // group_size_m - - offs_m = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) - offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) - offs_k = tl.arange(0, BLOCK_SIZE_K) - - mean = tl.load(Mean_ptr + offs_m, mask=offs_m < M, other=0.0) - rstd = tl.load(Rstd_ptr + offs_m, mask=offs_m < M, other=0.0) - - x_ptrs = X_ptr + (offs_m[:, None] * stride_xm + offs_k[None, :] * stride_xk) - wa_ptrs = W_ptr + (offs_k[:, None] * stride_wk + offs_n[None, :] * stride_wn) - wb_ptrs = W_ptr + (offs_k[:, None] * stride_wk + (N + offs_n[None, :]) * stride_wn) - - a_acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) - b_acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) - - for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K)): - kk = k * BLOCK_SIZE_K - k_remaining = K - kk - k_mask = offs_k < k_remaining - - x = tl.load( - x_ptrs, - mask=(offs_m[:, None] < M) & (offs_k[None, :] < k_remaining), - other=0.0, - ) - ln_w = tl.load(LN_W_ptr + kk + offs_k, mask=k_mask, other=0.0) - if HAS_LN_BIAS: - ln_b = tl.load(LN_B_ptr + kk + offs_k, mask=k_mask, other=0.0) - x_hat = ((x - mean[:, None]) * rstd[:, None]) * ln_w[None, :] + ln_b[ - None, : - ] - else: - x_hat = ((x - mean[:, None]) * rstd[:, None]) * ln_w[None, :] - - wa = tl.load( - wa_ptrs, - mask=(offs_k[:, None] < k_remaining) & (offs_n[None, :] < N), - other=0.0, - ) - wb = tl.load( - wb_ptrs, - mask=(offs_k[:, None] < k_remaining) & (offs_n[None, :] < N), - other=0.0, - ) - - a_acc = tl.dot(x_hat, wa, a_acc) - b_acc = tl.dot(x_hat, wb, b_acc) - - x_ptrs += BLOCK_SIZE_K * stride_xk - wa_ptrs += BLOCK_SIZE_K * stride_wk - wb_ptrs += BLOCK_SIZE_K * stride_wk - - a_bf = a_acc.to(Lin_ptr.type.element_ty) - b_bf = b_acc.to(Lin_ptr.type.element_ty) - - out_mask = (offs_m[:, None] < M) & (offs_n[None, :] < N) - lin_a_ptrs = ( - Lin_ptr + offs_m[:, None] * stride_lin_m + offs_n[None, :] * stride_lin_n - ) - lin_b_ptrs = ( - Lin_ptr + offs_m[:, None] * stride_lin_m + (N + offs_n[None, :]) * stride_lin_n - ) - tl.store(lin_a_ptrs, a_bf, mask=out_mask) - tl.store(lin_b_ptrs, b_bf, mask=out_mask) - - # SwiGLU = silu(a) * b. SiLU computed in fp32 for numerical accuracy. - sig = tl.sigmoid(a_acc) - silu_a = a_acc * sig - swiglu = silu_a * b_acc - out_ptrs = Out_ptr + offs_m[:, None] * stride_out_m + offs_n[None, :] * stride_out_n - tl.store(out_ptrs, swiglu.to(Out_ptr.type.element_ty), mask=out_mask) - - -# SwiGLU backward: in-place into the saved linear-output buffer (no alloc). -@triton.jit -def _swiglu_bwd_inplace_kernel( - dout_ptr, lin_ptr, M, N, stride_d, stride_l, BLOCK: tl.constexpr -): - row = tl.program_id(0).to(tl.int64) - cols = tl.arange(0, BLOCK) - mask = cols < N - - a_ptr = lin_ptr + row * stride_l + cols - b_ptr = lin_ptr + row * stride_l + N + cols - do_ptr = dout_ptr + row * stride_d + cols - - a = tl.load(a_ptr, mask=mask, other=0.0).to(tl.float32) - b = tl.load(b_ptr, mask=mask, other=0.0).to(tl.float32) - do = tl.load(do_ptr, mask=mask, other=0.0).to(tl.float32) - - sig = tl.sigmoid(a) - silu = a * sig - # d(silu)/da = sig + silu*(1 - sig) = sig*(1 + a*(1 - sig)) - silu_grad = sig * (1.0 + a * (1.0 - sig)) - - da = do * b * silu_grad - db = do * silu - - tl.store(a_ptr, da.to(lin_ptr.type.element_ty), mask=mask) - tl.store(b_ptr, db.to(lin_ptr.type.element_ty), mask=mask) - - -_FWD_CONFIG_K128 = dict( - BLOCK_SIZE_M=128, - BLOCK_SIZE_N=128, - BLOCK_SIZE_K=32, - GROUP_SIZE_M=8, - num_stages=4, - num_warps=8, -) -_FWD_CONFIG_K256 = dict( - BLOCK_SIZE_M=64, - BLOCK_SIZE_N=64, - BLOCK_SIZE_K=64, - GROUP_SIZE_M=8, - num_stages=3, - num_warps=4, -) - - -def _pick_fwd_config(K: int) -> dict: - if K == 128: - return _FWD_CONFIG_K128 - if K == 256: - return _FWD_CONFIG_K256 - # Reasonable default for other K. May be sub-optimal — autotune for new shapes. - return _FWD_CONFIG_K256 - - -def _next_pow2(n: int) -> int: - p = 1 - while p < n: - p <<= 1 - return p - - -def _ln_stats_settings(K: int) -> tuple[int, int]: - BLOCK = _next_pow2(K) - if BLOCK <= 256: - num_warps = 4 - elif BLOCK <= 1024: - num_warps = 8 - else: - num_warps = 16 - return BLOCK, num_warps - - -def _lnlin_swiglu_fwd( - x_2d: torch.Tensor, W12: torch.Tensor, LN_W: torch.Tensor, LN_B: torch.Tensor | None -): - assert x_2d.is_contiguous(), "X must be contiguous" - M, K = x_2d.shape - K2, two_N = W12.shape - assert K2 == K and two_N % 2 == 0, f"W12 shape mismatch: {W12.shape} vs K={K}" - N = two_N // 2 - - out = torch.empty((M, N), dtype=x_2d.dtype, device=x_2d.device) - lin = torch.empty((M, two_N), dtype=x_2d.dtype, device=x_2d.device) - Mean = torch.empty((M,), dtype=x_2d.dtype, device=x_2d.device) - Rstd = torch.empty((M,), dtype=x_2d.dtype, device=x_2d.device) - - block, num_warps = _ln_stats_settings(K) - _ln_stats_kernel[(M,)]( - x_2d, - x_2d.stride(0), - Mean, - Mean.stride(0), - Rstd, - Rstd.stride(0), - K, - 1e-5, - BLOCK_SIZE=block, - num_warps=num_warps, # pyright: ignore[reportCallIssue] - ) - - cfg = _pick_fwd_config(K) - grid = lambda META: ( - triton.cdiv(M, META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]), - ) - _lnlin_swiglu_fwd_kernel[grid]( - x_2d, - W12, - LN_W, - LN_B if LN_B is not None else LN_W, # ptr always non-null - lin, - out, - Mean, - Rstd, - M, - N, - K, - x_2d.stride(0), - x_2d.stride(1), - W12.stride(0), - W12.stride(1), - lin.stride(0), - lin.stride(1), - out.stride(0), - out.stride(1), - HAS_LN_BIAS=(LN_B is not None), - BLOCK_SIZE_M=cfg["BLOCK_SIZE_M"], - BLOCK_SIZE_N=cfg["BLOCK_SIZE_N"], - BLOCK_SIZE_K=cfg["BLOCK_SIZE_K"], - GROUP_SIZE_M=cfg["GROUP_SIZE_M"], - num_stages=cfg["num_stages"], # pyright: ignore[reportCallIssue] - num_warps=cfg["num_warps"], # pyright: ignore[reportCallIssue] - ) - return out, lin, Mean, Rstd - - -def _swiglu_bwd_inplace(dout: torch.Tensor, lin: torch.Tensor) -> torch.Tensor: - """In-place SwiGLU backward: writes da, db into the [M, 2N] `lin` buffer. - - Returns the same tensor (now containing dlin values).""" - M, N = dout.shape - BLOCK = _next_pow2(N) - _swiglu_bwd_inplace_kernel[(M,)]( - dout, - lin, - M, - N, - dout.stride(0), - lin.stride(0), - BLOCK=BLOCK, - num_warps=4, # pyright: ignore[reportCallIssue] - ) - return lin - - -class FusedLNLinearSwiGLUFunction(torch.autograd.Function): - """ - Forward: out = silu(x1) * x2 where (x1, x2) = chunk(LN(X) @ W12, 2) - Backward: standard chain via cuBLAS gemms + ATen native_layer_norm_backward. - """ - - @staticmethod - @torch.amp.custom_fwd(device_type="cuda", cast_inputs=torch.bfloat16) - def forward(ctx, X, W12, LN_W, LN_B): - x_shape = X.shape - x_2d = X.contiguous().view(-1, x_shape[-1]) - out, lin, mean, rstd = _lnlin_swiglu_fwd(x_2d, W12, LN_W, LN_B) - ctx.save_for_backward(x_2d, W12, LN_W, LN_B, mean, rstd, lin) - ctx.x_shape = x_shape - ctx.has_ln_bias = LN_B is not None - return out.view(*x_shape[:-1], out.shape[-1]) - - @staticmethod - @torch.amp.custom_bwd(device_type="cuda") - def backward(ctx, dout): - x_2d, W12, LN_W, LN_B, mean, rstd, lin = ctx.saved_tensors - dout_2d = dout.contiguous().view(-1, dout.shape[-1]) - K = x_2d.shape[1] - - # SwiGLU backward, in-place into the saved `lin` buffer (no new alloc). - dlin = _swiglu_bwd_inplace(dout_2d, lin) - - # LN+Linear backward via cuBLAS + ATen LN bwd. - # x_norm needed for dW12; recomputed via F.layer_norm (cuDNN, ~100 µs at d=256). - x_norm = F.layer_norm(x_2d, (K,), LN_W, LN_B, eps=1e-5) - dW12 = x_norm.transpose(0, 1) @ dlin # [K, 2N] - dx_norm = dlin @ W12.transpose(0, 1) # [M, K] - del x_norm - - # native_layer_norm_backward output_mask = [need_dX, need_dGamma, need_dBeta]. - # We always need dX and dGamma; dBeta only when LN bias was used. - output_mask = [True, True, ctx.has_ln_bias] - dX, dLN_W, dLN_B = torch.ops.aten.native_layer_norm_backward( - dx_norm, x_2d, [K], mean.float(), rstd.float(), LN_W, LN_B, output_mask - ) - # If LN_B was None (no bias), ATen returns dLN_B=None and we just pass it through. - return dX.view(ctx.x_shape), dW12, dLN_W, dLN_B - - -class FusedLNLinearSwiGLU(nn.Module): - """Fused module for `LayerNorm(d) -> Linear(d, 2*hidden) -> silu(x1)*x2`. - - Convention: silu of FIRST half. Output: [..., hidden].""" - - def __init__( - self, - d_model: int, - d_inner: int, - has_ln_bias: bool = True, - device=None, - dtype=None, - ): - super().__init__() - factory = {"device": device, "dtype": dtype} - self.d_model = d_model - self.d_inner = d_inner - self.LN_W = nn.Parameter(torch.empty(d_model, **factory)) - self.LN_B = ( - nn.Parameter(torch.empty(d_model, **factory)) if has_ln_bias else None - ) - self.W12 = nn.Parameter(torch.empty(d_model, 2 * d_inner, **factory)) - self.reset_parameters() - - def reset_parameters(self): - nn.init.ones_(self.LN_W) - if self.LN_B is not None: - nn.init.zeros_(self.LN_B) - bound = 1.0 / math.sqrt(self.d_model) - nn.init.uniform_(self.W12, -bound, bound) - - def forward(self, X: torch.Tensor) -> torch.Tensor: - return FusedLNLinearSwiGLUFunction.apply( # type: ignore[return-value] - X, self.W12, self.LN_W, self.LN_B - ) diff --git a/src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py b/src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py deleted file mode 100644 index 98c1b757c50e..000000000000 --- a/src/transformers/models/esmfold2/kernels/trimul_einsum_triton.py +++ /dev/null @@ -1,242 +0,0 @@ -"""Triton kernel for trimul stage-3 batched einsum in native (D, B, L, L) layout. - - outgoing: ``out[d,b,i,j] = sum_k a[d,b,i,k] * b[d,b,j,k]`` (TRANSPOSE_B=True) - incoming: ``out[d,b,i,j] = sum_k a[d,b,k,i] * b[d,b,k,j]`` (TRANSPOSE_A=True) - -All three tensors share the dense ``(D, B, L, L)`` row-major layout produced -upstream by ``fused_gated_dual_gemm_split`` and consumed downstream by -``layer_norm_transpose(..., layout="dbij->bijd")``. Operating natively in -this layout avoids the contiguous copy that ``torch.einsum``'s autograd -inserts in its backward when saved tensors don't match its expected layout. - -A single kernel covers all 4 transpose patterns (fwd + 4 bwd grad patterns) -via ``TRANSPOSE_A`` / ``TRANSPOSE_B`` constexpr flags. -""" - -import torch -import triton -import triton.language as tl - -# Static config — runtime autotune cold-start is unshippable for inference. -# Triton fwd loses to cuBLAS bgemm; bwd wins by avoiding torch.einsum's -# autograd-internal contiguous copy on (D, B, L, L) saved tensors. -_AUTOTUNE_CONFIGS = [ - triton.Config( - {"TILE_M": 128, "TILE_N": 128, "TILE_K": 64, "GROUP_M": 8}, - num_stages=3, - num_warps=8, - ) -] - - -@triton.autotune(configs=_AUTOTUNE_CONFIGS, key=["L", "TRANSPOSE_A", "TRANSPOSE_B"]) -@triton.jit -def _batched_einsum_kernel( - a_ptr, # (D, B, L, L) row-major - b_ptr, # (D, B, L, L) row-major - out_ptr, # (D, B, L, L) row-major - D, - B, - L, - stride_a_d, # = B*L*L - stride_a_b, # = L*L - stride_b_d, - stride_b_b, - stride_out_d, - stride_out_b, - TILE_M: tl.constexpr, - TILE_N: tl.constexpr, - TILE_K: tl.constexpr, - GROUP_M: tl.constexpr, - TRANSPOSE_A: tl.constexpr, - TRANSPOSE_B: tl.constexpr, -): - # Grid: (num_m * num_n, D * B); (m, n) swizzled for L2 reuse, (d, b) on axis-1. - pid_mn = tl.program_id(axis=0) - pid_db = tl.program_id(axis=1) - - pid_d = pid_db // B - pid_b = pid_db % B - - num_pid_m = tl.cdiv(L, TILE_M) - num_pid_n = tl.cdiv(L, TILE_N) - num_pid_in_group = GROUP_M * num_pid_n - group_id = pid_mn // num_pid_in_group - first_pid_m = group_id * GROUP_M - group_size_m = min(num_pid_m - first_pid_m, GROUP_M) - pid_m = first_pid_m + ((pid_mn % num_pid_in_group) % group_size_m) - pid_n = (pid_mn % num_pid_in_group) // group_size_m - - # int64 indexing — (D,B,L,L) at L=1024 D=128 is well over 2**31. - pid_d = tl.cast(pid_d, tl.int64) - pid_b = tl.cast(pid_b, tl.int64) - pid_m_i = tl.cast(pid_m, tl.int64) - pid_n_i = tl.cast(pid_n, tl.int64) - L_i = tl.cast(L, tl.int64) - - a_plane = a_ptr + pid_d * stride_a_d + pid_b * stride_a_b - b_plane = b_ptr + pid_d * stride_b_d + pid_b * stride_b_b - out_plane = out_ptr + pid_d * stride_out_d + pid_b * stride_out_b - - offs_m = pid_m_i * TILE_M + tl.arange(0, TILE_M).to(tl.int64) - offs_n = pid_n_i * TILE_N + tl.arange(0, TILE_N).to(tl.int64) - offs_k = tl.arange(0, TILE_K).to(tl.int64) - - mask_m = offs_m < L_i - mask_n = offs_n < L_i - - if TRANSPOSE_A: - a_ptrs = a_plane + (offs_k[:, None] * L_i + offs_m[None, :]) - a_step = TILE_K * L_i - else: - a_ptrs = a_plane + (offs_m[:, None] * L_i + offs_k[None, :]) - a_step = TILE_K - - if TRANSPOSE_B: - b_ptrs = b_plane + (offs_n[None, :] * L_i + offs_k[:, None]) - b_step = TILE_K - else: - b_ptrs = b_plane + (offs_k[:, None] * L_i + offs_n[None, :]) - b_step = TILE_K * L_i - - acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - n_k_tiles = tl.cdiv(L, TILE_K) - for kk in range(0, n_k_tiles): - k_remaining = L_i - kk * TILE_K - mask_k = offs_k < k_remaining - - if TRANSPOSE_A: - a_mask = mask_k[:, None] & mask_m[None, :] - a_tile = tl.load(a_ptrs, mask=a_mask, other=0.0) - a_tile = tl.trans(a_tile) # tl.dot wants (M, K) - else: - a_mask = mask_m[:, None] & mask_k[None, :] - a_tile = tl.load(a_ptrs, mask=a_mask, other=0.0) - - if TRANSPOSE_B: - b_mask = mask_k[:, None] & mask_n[None, :] - b_tile = tl.load(b_ptrs, mask=b_mask, other=0.0) - else: - b_mask = mask_k[:, None] & mask_n[None, :] - b_tile = tl.load(b_ptrs, mask=b_mask, other=0.0) - - acc = tl.dot(a_tile, b_tile, acc) - - a_ptrs += a_step - b_ptrs += b_step - - out_ptrs = out_plane + (offs_m[:, None] * L_i + offs_n[None, :]) - out_mask = mask_m[:, None] & mask_n[None, :] - tl.store(out_ptrs, acc.to(out_ptr.type.element_ty), mask=out_mask) - - -def _batched_einsum( - a: torch.Tensor, b: torch.Tensor, transpose_a: bool, transpose_b: bool -) -> torch.Tensor: - """Compute ``(A op_a) @ (B op_b)`` per (d, b) plane. - - Parameters - ---------- - a, b : torch.Tensor - Shape (D, B, L, L), bf16, contiguous. - transpose_a : bool - If True, contract over the row dim of A (k = leading), else over col dim. - transpose_b : bool - If True, B is read as (n, k) (i.e. B^T enters the matmul: out += A·B^T), - else as (k, n) (out += A·B). - - Returns - ------- - out : torch.Tensor - Shape (D, B, L, L), bf16. - """ - assert a.shape == b.shape, f"a {a.shape} ≠ b {b.shape}" - assert a.ndim == 4 - assert a.dtype == torch.bfloat16 and b.dtype == torch.bfloat16 - assert ( - a.is_contiguous() and b.is_contiguous() - ), "trimul einsum kernel requires contiguous (D,B,L,L) inputs" - - D, B, L_row, L_col = a.shape - assert L_row == L_col, "L_row must equal L_col for stage-3 einsum" - L = L_row - - out = torch.empty_like(a) - - stride_a_d, stride_a_b = a.stride(0), a.stride(1) - stride_b_d, stride_b_b = b.stride(0), b.stride(1) - stride_out_d, stride_out_b = out.stride(0), out.stride(1) - - def grid(meta): - num_m = triton.cdiv(L, meta["TILE_M"]) - num_n = triton.cdiv(L, meta["TILE_N"]) - return (num_m * num_n, D * B) - - _batched_einsum_kernel[grid]( - a, - b, - out, - D, - B, - L, - stride_a_d, - stride_a_b, - stride_b_d, - stride_b_b, - stride_out_d, - stride_out_b, - TRANSPOSE_A=transpose_a, - TRANSPOSE_B=transpose_b, - ) - return out - - -class TrimulBatchedEinsum(torch.autograd.Function): - """Autograd-aware wrapper for trimul stage-3 batched einsum. - - Forward direction is one of: - "outgoing": out[d,b,i,j] = sum_k a[d,b,i,k] * b[d,b,j,k] (A · B^T) - "incoming": out[d,b,i,j] = sum_k a[d,b,k,i] * b[d,b,k,j] (A^T · B) - """ - - @staticmethod - def forward( # type: ignore[override] - ctx, a: torch.Tensor, b: torch.Tensor, direction: str - ) -> torch.Tensor: - if direction == "outgoing": - out = _batched_einsum(a, b, transpose_a=False, transpose_b=True) - elif direction == "incoming": - out = _batched_einsum(a, b, transpose_a=True, transpose_b=False) - else: - raise ValueError(f"unknown direction {direction!r}") - ctx.save_for_backward(a, b) - ctx.direction = direction - return out - - @staticmethod - def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] - a, b = ctx.saved_tensors - direction = ctx.direction - if not grad_out.is_contiguous(): - grad_out = grad_out.contiguous() - - if direction == "outgoing": - grad_a = _batched_einsum(grad_out, b, transpose_a=False, transpose_b=False) - grad_b = _batched_einsum(grad_out, a, transpose_a=True, transpose_b=False) - else: # incoming - grad_a = _batched_einsum(b, grad_out, transpose_a=False, transpose_b=True) - grad_b = _batched_einsum(a, grad_out, transpose_a=False, transpose_b=False) - return grad_a, grad_b, None - - -def trimul_batched_einsum( - a: torch.Tensor, b: torch.Tensor, direction: str -) -> torch.Tensor: - """Public entry: dispatches to autograd Function if grad is enabled.""" - if torch.is_grad_enabled() and (a.requires_grad or b.requires_grad): - return TrimulBatchedEinsum.apply(a, b, direction) # type: ignore[return-value] - if direction == "outgoing": - return _batched_einsum(a, b, transpose_a=False, transpose_b=True) - elif direction == "incoming": - return _batched_einsum(a, b, transpose_a=True, transpose_b=False) - raise ValueError(f"unknown direction {direction!r}") diff --git a/src/transformers/models/esmfold2/kernels/trimul_with_residual.py b/src/transformers/models/esmfold2/kernels/trimul_with_residual.py deleted file mode 100644 index 45ec395c6169..000000000000 --- a/src/transformers/models/esmfold2/kernels/trimul_with_residual.py +++ /dev/null @@ -1,622 +0,0 @@ -"""TriMul with output residual + dropout-mask epilogue fused into the final GEMM. - -Inspired by cuequivariance's ``triangle_multiplicative_update`` -(https://docs.nvidia.com/cuda/cuequivariance/index.html); independently -re-implemented in Triton with the output residual and dropout mask folded -into the final gated GEMM so the ``delta = TriMul(pair)`` intermediate is -never written to HBM. - -Replaces the standard pattern - - pair_new = pair + dropout_mask * triangle_multiplicative_update(pair) - -— where the two ops materialize the full ``delta = TriMul(pair)`` tensor -between them — with a single fused path. The final gated GEMM fuses three -operations end-to-end: - - 1. Computes ``delta = sigmoid(x_in @ Wg) * (x_out @ Wp)`` per output tile. - 2. Multiplies by the row-shared dropout mask in-register - (``mask[b, j, d]`` indexed by decoded ``b, j`` from the flat ``m`` index). - 3. Adds the prior pair residual loaded from HBM. - 4. Writes the new pair tensor. - -Saves: one full pair-tensor write (``delta``) + one full pair-tensor read -(``delta`` re-read by FusedDropoutResidual). Roughly ~250 MB per call at -L=768, B=5 in bf16; compounds across loops. - -Forward + backward both implemented for the bf16 path. fp8 IO paths and -``transpose_out`` remain inference-only. The backward kernel recomputes -``gate = sigmoid(x1 @ w1.T)`` and ``val = x2 @ w2.T`` (the two dual GEMMs) -to keep saved-context cost down, and emits per-element -``d_gate_logits``, ``d_val_acc``, ``d_drop_mask_partials``; outer cuBLAS -calls handle the weight/input GEMM reductions. The residual add is -identity in the backward: ``d_residual = d_out``. -""" -# ruff: noqa: E402 - -from __future__ import annotations - -import os -import warnings - -warnings.filterwarnings("ignore", category=FutureWarning) -warnings.filterwarnings("ignore", category=UserWarning) -warnings.filterwarnings("ignore", category=DeprecationWarning) -os.environ.setdefault("CUEQ_DEFAULT_CONFIG", "1") -os.environ.setdefault("CUEQ_DISABLE_AOT_TUNING", "1") - -import torch -import triton -import triton.language as tl - -from .fused_dual_gemm import fused_gated_dual_gemm_split -from .fused_ln_residual import fused_ln_transpose, fused_ln_with_residual_link -from .trimul_einsum_triton import trimul_batched_einsum - -# Static config — runtime autotune cold-start is unshippable for inference. -_AUTOTUNE_CONFIGS = [ - triton.Config( - {"TILE_M": 64, "TILE_N": 64, "TILE_K": 64, "GROUP_M": 8}, - num_stages=3, - num_warps=4, - ) -] - - -@triton.autotune(configs=_AUTOTUNE_CONFIGS, key=["M", "N", "K", "HAS_DROP_MASK"]) -@triton.jit -def _gated_gemm_with_residual_kernel( - x1_ptr, # [M, K] — gate input (pre-residual pair) - x2_ptr, # [M, K] — value input (post-LN_out) - w1_ptr, # [N, K] — gate weight - w2_ptr, # [N, K] — value weight - residual_ptr, # [M, N] pair tensor pre-TriMul - drop_mask_ptr, # [B*N_COL, N] row-shared dropout mask - o_ptr, # [M, N] output = residual + drop_mask * sigmoid(x1@w1) * (x2@w2) - M, - N, - K, - STRIDE_B: tl.constexpr, # = N_ROW * N_COL (for decoding b from m) - N_COL: tl.constexpr, # = L_col (for decoding j from m) - TILE_M: tl.constexpr, - TILE_N: tl.constexpr, - TILE_K: tl.constexpr, - GROUP_M: tl.constexpr, - PRECISION: tl.constexpr, - HAS_DROP_MASK: tl.constexpr, - NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] -): - pid_m_raw = tl.program_id(axis=0) - pid_n_raw = tl.program_id(axis=1) - - # GROUP_M swizzle for L2 reuse of x rows across consecutive CTAs. - num_pid_m = tl.cdiv(M, TILE_M) - num_pid_n = tl.cdiv(N, TILE_N) - pid = pid_n_raw * num_pid_m + pid_m_raw - num_pid_in_group = GROUP_M * num_pid_n - group_id = pid // num_pid_in_group - first_pid_m = group_id * GROUP_M - group_size_m = min(num_pid_m - first_pid_m, GROUP_M) - pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) - pid_n = (pid % num_pid_in_group) // group_size_m - - if NEEDS_INT64: - pid_m = tl.cast(pid_m, tl.int64) - pid_n = tl.cast(pid_n, tl.int64) - M = tl.cast(M, tl.int64) - N = tl.cast(N, tl.int64) - K = tl.cast(K, tl.int64) - - start_m = pid_m * TILE_M - start_n = pid_n * TILE_N - - offs_xm = start_m + tl.arange(0, TILE_M) - offs_wn = start_n + tl.arange(0, TILE_N) - offs_k = tl.arange(0, TILE_K) - - if NEEDS_INT64: - offs_xm = tl.cast(offs_xm, tl.int64) - offs_wn = tl.cast(offs_wn, tl.int64) - offs_k = tl.cast(offs_k, tl.int64) - - x1_ptrs = x1_ptr + (offs_xm[:, None] * K + offs_k[None, :]) - x2_ptrs = x2_ptr + (offs_xm[:, None] * K + offs_k[None, :]) - w_tile_offs = offs_wn[None, :] * K + offs_k[:, None] - - acc_1 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - acc_2 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - mask_m = offs_xm < M - - for _ in range(0, tl.cdiv(K, TILE_K)): - x1 = tl.load(x1_ptrs, mask=mask_m[:, None], other=0.0).to( - w1_ptr.type.element_ty - ) - w1_ptrs = w1_ptr + w_tile_offs - w1 = tl.load(w1_ptrs) - if PRECISION == 0: - acc_1 = tl.dot(x1, w1, acc_1) - elif PRECISION == 2: - acc_1 = tl.dot(x1, w1, acc_1, input_precision="tf32x3") - else: - acc_1 = tl.dot(x1, w1, acc_1, input_precision="ieee") - x1_ptrs += TILE_K - w1_ptr += TILE_K - - for _ in range(0, tl.cdiv(K, TILE_K)): - x2 = tl.load(x2_ptrs, mask=mask_m[:, None], other=0.0).to( - w2_ptr.type.element_ty - ) - w2_ptrs = w2_ptr + w_tile_offs - w2 = tl.load(w2_ptrs) - if PRECISION == 0: - acc_2 = tl.dot(x2, w2, acc_2) - elif PRECISION == 2: - acc_2 = tl.dot(x2, w2, acc_2, input_precision="tf32x3") - else: - acc_2 = tl.dot(x2, w2, acc_2, input_precision="ieee") - x2_ptrs += TILE_K - w2_ptr += TILE_K - - acc_1 = 1.0 / (1.0 + tl.exp(-acc_1)) - delta = acc_1 * acc_2 - - offs_om = pid_m * TILE_M + tl.arange(0, TILE_M) - offs_on = pid_n * TILE_N + tl.arange(0, TILE_N) - if NEEDS_INT64: - offs_om = tl.cast(offs_om, tl.int64) - offs_on = tl.cast(offs_on, tl.int64) - - # Row-shared dropout: mask is [B*N_COL, N]; decode (b, j) from m. - if HAS_DROP_MASK: - b = offs_om // STRIDE_B - j = offs_om % N_COL - mask_row = b * N_COL + j - drop_ptrs = drop_mask_ptr + mask_row[:, None] * N + offs_on[None, :] - drop_tile = tl.load(drop_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - delta = delta * drop_tile - - resid_ptrs = residual_ptr + offs_om[:, None] * N + offs_on[None, :] - resid_tile = tl.load(resid_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - out_val = delta + resid_tile - - o_ptrs = o_ptr + offs_om[:, None] * N + offs_on[None, :] - o_mask = mask_m[:, None] - tl.store(o_ptrs, out_val.to(o_ptr.type.element_ty), mask=o_mask) - - -# Backward recomputes the two fwd GEMMs, emits per-element grads for the -# outer cuBLAS GEMMs (d_x1, d_x2, d_w1, d_w2). d_residual is identity through -# the residual add. d_drop_mask is row-shared → per-pid_n partial + host reduce. - - -# Narrow TILE_N=32 in bwd: register pressure is dominated by the two output -# gradient tensors (vs forward's single output). -_BWD_AUTOTUNE_CONFIGS = [ - triton.Config( - {"TILE_M": 64, "TILE_N": 32, "TILE_K": 64, "GROUP_M": 8}, - num_stages=3, - num_warps=4, - ) -] - - -@triton.autotune(configs=_BWD_AUTOTUNE_CONFIGS, key=["M", "N", "K", "HAS_DROP_MASK"]) -@triton.jit -def _gated_gemm_with_residual_backward_kernel( - grad_out_ptr, # [M, N] bf16 — incoming grad - x1_ptr, # [M, K] bf16 — saved gate input - x2_ptr, # [M, K] bf16 — saved value input - w1_ptr, # [N, K] bf16 - w2_ptr, # [N, K] bf16 - drop_mask_ptr, # [B*N_COL, N] bf16 — row-shared (unused if HAS_DROP_MASK=0) - grad_gate_logits_ptr, # [M, N] bf16 — out - grad_val_acc_ptr, # [M, N] bf16 — out - grad_drop_mask_buf_ptr, # [B*N_COL, N] fp32 — drop_mask grad accumulator (atomic_add) - M, - N, - K, - STRIDE_B: tl.constexpr, - N_COL: tl.constexpr, - TILE_M: tl.constexpr, - TILE_N: tl.constexpr, - TILE_K: tl.constexpr, - GROUP_M: tl.constexpr, - HAS_DROP_MASK: tl.constexpr, - NEEDS_INT64: tl.constexpr = True, # type: ignore[assignment] -): - pid_m_raw = tl.program_id(axis=0) - pid_n_raw = tl.program_id(axis=1) - - num_pid_m = tl.cdiv(M, TILE_M) - num_pid_n = tl.cdiv(N, TILE_N) - pid = pid_n_raw * num_pid_m + pid_m_raw - num_pid_in_group = GROUP_M * num_pid_n - group_id = pid // num_pid_in_group - first_pid_m = group_id * GROUP_M - group_size_m = min(num_pid_m - first_pid_m, GROUP_M) - pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) - pid_n = (pid % num_pid_in_group) // group_size_m - - if NEEDS_INT64: - pid_m = tl.cast(pid_m, tl.int64) - pid_n = tl.cast(pid_n, tl.int64) - M = tl.cast(M, tl.int64) - N = tl.cast(N, tl.int64) - K = tl.cast(K, tl.int64) - - start_m = pid_m * TILE_M - start_n = pid_n * TILE_N - - offs_xm = start_m + tl.arange(0, TILE_M) - offs_wn = start_n + tl.arange(0, TILE_N) - offs_k = tl.arange(0, TILE_K) - if NEEDS_INT64: - offs_xm = tl.cast(offs_xm, tl.int64) - offs_wn = tl.cast(offs_wn, tl.int64) - offs_k = tl.cast(offs_k, tl.int64) - - x1_ptrs = x1_ptr + (offs_xm[:, None] * K + offs_k[None, :]) - x2_ptrs = x2_ptr + (offs_xm[:, None] * K + offs_k[None, :]) - w_tile_offs = offs_wn[None, :] * K + offs_k[:, None] - - acc_1 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - acc_2 = tl.zeros((TILE_M, TILE_N), dtype=tl.float32) - mask_m = offs_xm < M - - w1p = w1_ptr - for _ in range(0, tl.cdiv(K, TILE_K)): - x1 = tl.load(x1_ptrs, mask=mask_m[:, None], other=0.0).to( - w1_ptr.type.element_ty - ) - w1 = tl.load(w1p + w_tile_offs) - acc_1 = tl.dot(x1, w1, acc_1) - x1_ptrs += TILE_K - w1p += TILE_K - - w2p = w2_ptr - for _ in range(0, tl.cdiv(K, TILE_K)): - x2 = tl.load(x2_ptrs, mask=mask_m[:, None], other=0.0).to( - w2_ptr.type.element_ty - ) - w2 = tl.load(w2p + w_tile_offs) - acc_2 = tl.dot(x2, w2, acc_2) - x2_ptrs += TILE_K - w2p += TILE_K - - g = tl.sigmoid(acc_1) - delta = g * acc_2 # pre-mask delta (forward post-mask = delta * drop_mask) - - offs_om = pid_m * TILE_M + tl.arange(0, TILE_M) - offs_on = pid_n * TILE_N + tl.arange(0, TILE_N) - if NEEDS_INT64: - offs_om = tl.cast(offs_om, tl.int64) - offs_on = tl.cast(offs_on, tl.int64) - - grad_o_ptrs = grad_out_ptr + offs_om[:, None] * N + offs_on[None, :] - grad_o = tl.load(grad_o_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - - if HAS_DROP_MASK: - # Multiple m's collide on the same mask_row, so use fp32 atomic_add to a - # (B*N_COL, N) accumulator (handles intra- and inter-tile collisions). - b = offs_om // STRIDE_B - j = offs_om % N_COL - mask_row = b * N_COL + j - drop_ptrs = drop_mask_ptr + mask_row[:, None] * N + offs_on[None, :] - drop_tile = tl.load(drop_ptrs, mask=mask_m[:, None], other=0.0).to(tl.float32) - - d_drop_per_elem = grad_o * delta - acc_ptrs = grad_drop_mask_buf_ptr + mask_row[:, None] * N + offs_on[None, :] - tl.atomic_add(acc_ptrs, d_drop_per_elem, mask=mask_m[:, None]) - - grad_o = grad_o * drop_tile - - d_val_acc = (grad_o * g).to(grad_val_acc_ptr.type.element_ty) - d_val_acc_ptrs = grad_val_acc_ptr + offs_om[:, None] * N + offs_on[None, :] - tl.store(d_val_acc_ptrs, d_val_acc, mask=mask_m[:, None]) - - d_gate_logits = (grad_o * acc_2 * g * (1.0 - g)).to( - grad_gate_logits_ptr.type.element_ty - ) - d_gate_logits_ptrs = grad_gate_logits_ptr + offs_om[:, None] * N + offs_on[None, :] - tl.store(d_gate_logits_ptrs, d_gate_logits, mask=mask_m[:, None]) - - -def _gated_gemm_with_residual_bwd( - grad_out: torch.Tensor, # [M, N] bf16 - x1: torch.Tensor, # [M, K] bf16 - x2: torch.Tensor, # [M, K] bf16 - w1: torch.Tensor, - w2: torch.Tensor, - drop_mask: torch.Tensor | None, - n_row: int, - n_col: int, -) -> tuple[ - torch.Tensor, - torch.Tensor, - torch.Tensor, - torch.Tensor, - torch.Tensor, - torch.Tensor | None, -]: - """Returns (d_x1, d_x2, d_w1, d_w2, d_residual, d_drop_mask).""" - M, K = x1.shape - N = w1.shape[0] - assert x2.shape == x1.shape - assert ( - w1.dtype == w2.dtype == torch.bfloat16 - ), f"weights must be bf16; got {w1.dtype}/{w2.dtype}" - assert x1.dtype == torch.bfloat16 and x2.dtype == torch.bfloat16 - - # Don't call .contiguous() unconditionally (avoids a full-tensor clone). - if not grad_out.is_contiguous(): - grad_out = grad_out.contiguous() - grad_out_2d = grad_out.view(M, N) - x1_c = x1 if x1.is_contiguous() else x1.contiguous() - x2_c = x2 if x2.is_contiguous() else x2.contiguous() - w1_c = w1 if w1.is_contiguous() else w1.contiguous() - w2_c = w2 if w2.is_contiguous() else w2.contiguous() - - grad_gate_logits = torch.empty((M, N), device=x1.device, dtype=torch.bfloat16) - grad_val_acc = torch.empty((M, N), device=x1.device, dtype=torch.bfloat16) - - # fp32 accumulator for row-shared mask_row collisions across m. - if drop_mask is not None: - drop_mask = drop_mask.contiguous().view(-1, N) - n_mask_rows = drop_mask.shape[0] - grad_drop_buf = torch.zeros( - (n_mask_rows, N), device=x1.device, dtype=torch.float32 - ) - else: - grad_drop_buf = torch.empty((1,), device=x1.device, dtype=torch.float32) - - _dummy_mask = torch.zeros((), device=x1.device, dtype=torch.bfloat16) - - NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * N >= 2**31 - 1) - - def grid(meta): - return (triton.cdiv(M, meta["TILE_M"]), triton.cdiv(N, meta["TILE_N"])) - - _gated_gemm_with_residual_backward_kernel[grid]( - grad_out_2d, - x1_c, - x2_c, - w1_c, - w2_c, - drop_mask if drop_mask is not None else _dummy_mask, - grad_gate_logits, - grad_val_acc, - grad_drop_buf, - M, - N, - K, - STRIDE_B=n_row * n_col, - N_COL=n_col, - HAS_DROP_MASK=drop_mask is not None, - NEEDS_INT64=NEEDS_INT64, - ) - - d_w1 = grad_gate_logits.t() @ x1_c # [N, K] - d_w2 = grad_val_acc.t() @ x2_c # [N, K] - d_x1 = grad_gate_logits @ w1_c # [M, K] - d_x2 = grad_val_acc @ w2_c # [M, K] - - d_residual = grad_out_2d - - if drop_mask is not None: - d_drop_mask = grad_drop_buf.to(drop_mask.dtype) - else: - d_drop_mask = None - - return d_x1, d_x2, d_w1, d_w2, d_residual, d_drop_mask - - -class GatedGEMMWithResidual(torch.autograd.Function): - """Autograd wrapper for ``_gated_gemm_with_residual`` (bf16 path).""" - - @staticmethod - def forward( - ctx, - x1: torch.Tensor, - x2: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - residual: torch.Tensor, - drop_mask: torch.Tensor | None, - n_row: int, - n_col: int, - precision: int, - ) -> torch.Tensor: - out = _gated_gemm_with_residual_fwd( - x1, x2, w1, w2, residual, drop_mask, n_row, n_col, precision=precision - ) - if drop_mask is None: - ctx.save_for_backward(x1, x2, w1, w2) - ctx.has_drop_mask = False - else: - ctx.save_for_backward(x1, x2, w1, w2, drop_mask) - ctx.has_drop_mask = True - ctx.n_row = n_row - ctx.n_col = n_col - return out - - @staticmethod - def backward(ctx, grad_out: torch.Tensor): # type: ignore[override] - if ctx.has_drop_mask: - x1, x2, w1, w2, drop_mask = ctx.saved_tensors - else: - x1, x2, w1, w2 = ctx.saved_tensors - drop_mask = None - d_x1, d_x2, d_w1, d_w2, d_res, d_drop = _gated_gemm_with_residual_bwd( - grad_out.contiguous(), x1, x2, w1, w2, drop_mask, ctx.n_row, ctx.n_col - ) - return d_x1, d_x2, d_w1, d_w2, d_res, d_drop, None, None, None - - -def _gated_gemm_with_residual_fwd( - x1: torch.Tensor, - x2: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - residual: torch.Tensor, - drop_mask: torch.Tensor | None, - n_row: int, - n_col: int, - precision: int = 0, -) -> torch.Tensor: - """Run the residual+dropout-aware final GEMM. - - Shapes: x1, x2: (M, K). w1, w2: (N, K). residual: (M, N). drop_mask: (B*n_col, N) or None. - Output: (M, N). - """ - M, K = x1.shape - N = w1.shape[0] - x1 = x1.contiguous() - x2 = x2.contiguous() - w1 = w1.contiguous() - w2 = w2.contiguous() - residual = residual.contiguous().view(M, N) - if drop_mask is not None: - drop_mask = drop_mask.contiguous().view(-1, N) - out = residual.new_empty((M, N)) - - NEEDS_INT64 = (M * K >= 2**31 - 1) or (M * N >= 2**31 - 1) - - def grid(meta): - return (triton.cdiv(M, meta["TILE_M"]), triton.cdiv(N, meta["TILE_N"])) - - _gated_gemm_with_residual_kernel[grid]( - x1, - x2, - w1, - w2, - residual, - drop_mask if drop_mask is not None else residual, # dummy pass-through - out, - M, - N, - K, - STRIDE_B=n_row * n_col, - N_COL=n_col, - PRECISION=precision, - HAS_DROP_MASK=drop_mask is not None, - NEEDS_INT64=NEEDS_INT64, - ) - return out - - -def _gated_gemm_with_residual( - x1: torch.Tensor, - x2: torch.Tensor, - w1: torch.Tensor, - w2: torch.Tensor, - residual: torch.Tensor, - drop_mask: torch.Tensor | None, - n_row: int, - n_col: int, - precision: int = 0, -) -> torch.Tensor: - """Public wrapper: dispatches to autograd Function if grad is enabled.""" - if torch.is_grad_enabled() and ( - x1.requires_grad - or x2.requires_grad - or w1.requires_grad - or w2.requires_grad - or residual.requires_grad - ): - return GatedGEMMWithResidual.apply( # type: ignore[return-value] - x1, x2, w1, w2, residual, drop_mask, n_row, n_col, precision - ) - return _gated_gemm_with_residual_fwd( - x1, x2, w1, w2, residual, drop_mask, n_row, n_col, precision=precision - ) - - -@torch._dynamo.disable -def triangle_multiplicative_update_with_residual( - pair: torch.Tensor, - direction: str, - residual: torch.Tensor, - drop_mask: torch.Tensor | None, - *, - norm_in_weight: torch.Tensor, - norm_in_bias: torch.Tensor, - p_in_weight: torch.Tensor, - g_in_weight: torch.Tensor, - norm_out_weight: torch.Tensor, - norm_out_bias: torch.Tensor, - p_out_weight: torch.Tensor, - g_out_weight: torch.Tensor, - mask: torch.Tensor | None = None, - eps: float = 1e-5, - precision: int = 0, -) -> torch.Tensor: - """Fused TriMul-and-residual: pair_new = residual + drop_mask * TriMul(pair). - - The intermediate ``delta = TriMul(pair)`` is never materialized in HBM — - the dropout-mask multiply and residual-add happen in-kernel at the final - GEMM. Saves one pair-tensor write + read vs the - ``TriMul → FusedDropoutResidual`` baseline. - - Shapes: - pair, residual: (B, L, L, c_z) - drop_mask: (B, 1, L, c_z) or (B*L, c_z) — row-shared - mask: (B, L, L) or None - weights: (N, K) bf16 — see arg list for each stage. - """ - assert pair.shape == residual.shape - B, L_row, L_col, c_z = pair.shape - - # Stage 1: input LayerNorm. When ``residual is pair`` (common case for trimul), - # use the fused LN-fwd + LN-bwd-with-residual variant so the bwd pass adds - # ``grad_residual`` into ``grad_x`` in one kernel (saves one pair-tensor - # HBM round-trip). - if residual is pair: - x, residual_alias = fused_ln_with_residual_link( - pair, norm_in_weight, norm_in_bias, pair, eps=eps, layout="bijd->bijd" - ) - else: - x = fused_ln_transpose( - pair, norm_in_weight, norm_in_bias, eps=eps, layout="bijd->bijd" - ) - residual_alias = residual - x_in = x - - # Stage 2: gated dual GEMM → ab (transposed for the einsum's dbij layout). - # Train path runs transpose_out=False (no bwd for in-kernel transpose), - # then a view/permute reaches the dbij layout downstream. - a, b_t = fused_gated_dual_gemm_split(x, g_in_weight, p_in_weight, mask=mask) - - # Stage 3: triangular einsum. - # Training: native (D, B, L, L) Triton kernel — its bwd reads the layout - # natively, eliminating the contiguous copy torch.einsum's autograd does. - # Inference: torch.einsum (cuBLAS bgemm) is faster fwd-only. - if torch.is_grad_enabled(): - x = trimul_batched_einsum(a, b_t, direction) - elif direction == "outgoing": - x = torch.einsum("dbik,dbjk->dbij", a, b_t) - else: - x = torch.einsum("dbki,dbkj->dbij", a, b_t) - - # Stage 4: output LayerNorm (back to bijd). - x_out = fused_ln_transpose( - x, norm_out_weight, norm_out_bias, eps=eps, layout="dbij->bijd" - ) - - # Stage 5: fused output gated GEMM + dropout mask + residual add. - x_in_2d = x_in.reshape(-1, c_z) - x_out_2d = x_out.reshape(-1, c_z) - # Stage-5 residual MUST be the LN1 alias when LN1-residual-fusion is on, - # otherwise the grad routing through the fused LN bwd won't fire. - residual_2d = residual_alias.reshape(-1, c_z) - out_2d = _gated_gemm_with_residual( - x_in_2d, - x_out_2d, - g_out_weight, - p_out_weight, - residual_2d, - drop_mask, - n_row=L_row, - n_col=L_col, - precision=precision, - ) - return out_2d.view(B, L_row, L_col, c_z) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 79024feb0285..17f1e39b1d7a 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -146,9 +146,6 @@ def __init__(self, config: "ESMFold2Config") -> None: torch.zeros(max_atoms_per_token, d_single, 2) ) - def set_kernel_backend(self, backend: str | None) -> None: - self.folding_trunk.set_kernel_backend(backend) - def set_chunk_size(self, chunk_size: int | None) -> None: self.folding_trunk.set_chunk_size(chunk_size) @@ -508,8 +505,6 @@ class ESMFold2Model(PreTrainedModel): * ``model.set_chunk_size(int|None)``: caps L² ops (triangle / OPM / pair transition) at this token-axis chunk. Default 64 — fits L≈2k on an 80 GB GPU. Pass ``None`` for faster inference at L<600. - * ``model.set_kernel_backend(None | "fused" | "cuequivariance")``: - select kernel backend (None = reference path). """ config_class = ESMFold2Config @@ -651,29 +646,10 @@ def from_pretrained( model.load_esmc(model.config.esmc_id, precision=esmc_precision) return model - def set_kernel_backend(self, backend: str | None) -> None: - """Select kernel backend. - - Args: - backend: ``None`` (reference path), ``"fused"`` (vendored Triton - kernels), or ``"cuequivariance"`` (cuequivariance kernels - where applicable; vanilla python fallback otherwise). - """ - self.folding_trunk.set_kernel_backend(backend) - if self.lm_encoder is not None: - self.lm_encoder.set_kernel_backend(backend) - self.parcae_coda.set_kernel_backend(backend) - self.confidence_head.set_kernel_backend(backend) - self.structure_head.set_kernel_backend(backend) - def apply_torch_compile( self, mode: str = "fixed_seqlen", dynamic: bool | None = None ) -> None: - """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once. - - Does NOT stack with our Triton kernels — call ``set_kernel_backend(None)`` - before compiling. - """ + """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" import torch._dynamo torch._dynamo.config.cache_size_limit = 512 # type: ignore[attr-defined] diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index a7789647dbae..1f2e0454ecfc 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -40,90 +40,21 @@ pad_input = None # type: ignore[assignment] FLASH_ATTN_AVAILABLE = False -try: - from cuequivariance_torch import ( # type: ignore[import] - attention_pair_bias as _cue_attn_pair_bias, - ) - from cuequivariance_torch.primitives.triangle import ( # type: ignore[import] - triangle_multiplicative_update as _cue_tri_mul, - ) - - CUE_AVAILABLE = True -except ImportError: - _cue_attn_pair_bias = None # type: ignore[assignment] - _cue_tri_mul = None # type: ignore[assignment] - CUE_AVAILABLE = False - -# Vendored inference-only Triton kernels. -try: - from .kernels import ( - FusedDropoutResidual as _FusedDropoutResidual, # type: ignore[import] - ) - from .kernels import ( - FusedLNLinearSwiGLU as _FusedLNLinearSwiGLU, # type: ignore[import] - ) - from .kernels import fused_pair_bias as _fused_pair_bias # type: ignore[import] - from .kernels import ( # type: ignore[import] - triangle_multiplicative_update_with_residual as _fused_trimul_with_residual, - ) - - TRITON_KERNELS_AVAILABLE = True -except ImportError: - _fused_pair_bias = None # type: ignore[assignment] - _fused_trimul_with_residual = None # type: ignore[assignment] - _FusedLNLinearSwiGLU = None # type: ignore[assignment] - _FusedDropoutResidual = None # type: ignore[assignment] - TRITON_KERNELS_AVAILABLE = False - from .configuration_esmfold2 import ESMFold2Config -BACKEND_FUSED = "fused" -BACKEND_CUEQ = "cuequivariance" -_VALID_BACKENDS = (None, BACKEND_FUSED, BACKEND_CUEQ) - - -def _fused_active(module: nn.Module, tensor: Tensor) -> bool: - """Common preconditions for the vendored fused Triton inference kernels.""" - return ( - TRITON_KERNELS_AVAILABLE - and getattr(module, "_kernel_backend", None) == BACKEND_FUSED - and not torch.is_grad_enabled() - and tensor.is_cuda - ) - - -def _cueq_active(module: nn.Module) -> bool: - return CUE_AVAILABLE and getattr(module, "_kernel_backend", None) == BACKEND_CUEQ - class DropoutResidual(nn.Module): - """``residual + dropout(delta)`` with row/col-shared dropout. - - Same signature on both paths. ``use_fused_kernels=True`` + ``batch_dim=1`` - routes through ``FusedDropoutResidual`` (single-pass over pair tensor, - in-place residual add). Falls back to unfused otherwise. - """ + """``residual + dropout(delta)`` with row/col-shared dropout.""" - def __init__( - self, r: float, batch_dim: int, use_fused_kernels: bool = False - ) -> None: + def __init__(self, r: float, batch_dim: int) -> None: super().__init__() assert batch_dim in (1, 2), f"batch_dim must be 1 or 2, got {batch_dim}" - self._use_fused_kernels = ( - use_fused_kernels and batch_dim == 1 and _FusedDropoutResidual is not None - ) self._batch_dim = batch_dim self._r = r - if self._use_fused_kernels: - assert _FusedDropoutResidual is not None - self._impl: nn.Module = _FusedDropoutResidual(r) - else: - self._impl = nn.Dropout(r) + self._impl = nn.Dropout(r) def forward(self, residual: Tensor, delta: Tensor) -> Tensor: - if self._use_fused_kernels: - return self._impl(residual, delta) - # Unfused: row/col-shared dropout via [1, ...] mask broadcast. + # Row/col-shared dropout via [1, ...] mask broadcast. if self._r == 0.0 or not self.training: return residual + delta shape = list(delta.shape) @@ -983,42 +914,6 @@ def __init__( self.pair_norm = nn.LayerNorm(d_pair, eps=1e-5) self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) - self._kernel_backend: str | None = None - - def set_kernel_backend(self, backend: str | None) -> None: - if backend not in _VALID_BACKENDS: - raise ValueError( - f"backend must be one of {_VALID_BACKENDS}, got {backend!r}" - ) - self._kernel_backend = backend - - def _is_zero_beta(self, beta: Tensor | float) -> bool: - if isinstance(beta, (int, float)): - return beta == 0.0 - return bool((beta == 0).all()) - - def _can_use_fused_pair_bias( - self, z: Tensor, n_queries: int, beta: Tensor | float - ) -> bool: - return ( - _fused_active(self, z) - and z.dim() == 4 - and self._is_zero_beta(beta) - and hasattr(self, "pair_bias_proj") - and hasattr(self, "pair_norm") - ) - - def _can_use_cueq_pair_bias( - self, z: Tensor, n_queries: int, beta: Tensor | float - ) -> bool: - return ( - _cueq_active(self) - and n_queries > 750 - and z.dim() == 4 - and self._is_zero_beta(beta) - and hasattr(self, "pair_bias_proj") - ) - def forward( self, a: Tensor, @@ -1054,91 +949,30 @@ def forward( num_diffusion_samples, dim=0 ) - if self._can_use_fused_pair_bias(z, n_queries, beta): - kernel_mask = ( - attention_mask - if attention_mask is not None - else torch.ones(bsz, n_queries, device=a.device, dtype=torch.bool) - ) - pair_norm_w = self.pair_norm.weight - pair_norm_b = ( - self.pair_norm.bias - if self.pair_norm.bias is not None - else torch.zeros_like(pair_norm_w) - ) - z_bf = z if z.dtype == torch.bfloat16 else z.to(torch.bfloat16) - bias = _fused_pair_bias( # type: ignore[misc] - z_bf, - kernel_mask, - self.pair_bias_proj.weight, - num_heads=self.num_heads, - pair_norm_w=pair_norm_w, - pair_norm_b=pair_norm_b, - ) # (B, H, Q, K) - q_bhqd = q.transpose(1, 2) - k_bhqd = k.transpose(1, 2) - v_bhqd = v.transpose(1, 2) - attn_out = F.scaled_dot_product_attention( - q_bhqd, k_bhqd, v_bhqd, attn_mask=bias.to(q_bhqd.dtype) - ) - g = torch.sigmoid(self.g_proj(x)).view( - bsz, n_queries, self.num_heads, self.head_dim - ) - ctx = g * attn_out.transpose(1, 2) - out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) - if s is not None: - out = torch.sigmoid(self.out_gate(s)) * out - return out - - if self._can_use_cueq_pair_bias(z, n_queries, beta): - kernel_mask = ( - attention_mask - if attention_mask is not None - else torch.ones(bsz, n_queries, device=a.device, dtype=torch.bool) - ) - out, _ = _cue_attn_pair_bias( # type: ignore[misc] - s=x, - q=q.transpose(1, 2), - k=k.transpose(1, 2), - v=v.transpose(1, 2), - z=z, - mask=kernel_mask, - num_heads=self.num_heads, - w_proj_z=self.pair_bias_proj.weight, - w_proj_g=self.g_proj.weight, - w_proj_o=self.out_proj.weight, - w_ln_z=self.pair_norm.weight, - b_ln_z=self.pair_norm.bias, - return_z_proj=False, - is_cached_z_proj=False, - ) - else: - # Standard attention with pair bias - g = torch.sigmoid(self.g_proj(x)).view( - bsz, n_queries, self.num_heads, self.head_dim - ) + # Standard attention with pair bias + g = torch.sigmoid(self.g_proj(x)).view( + bsz, n_queries, self.num_heads, self.head_dim + ) - logits = ( - torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale - ) + logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale - if z.dim() == 4: - pair_bias = self.pair_bias_proj(self.pair_norm(z)) - else: - pair_bias = z.unsqueeze(-1) - logits = logits + pair_bias.to(dtype=logits.dtype) + if z.dim() == 4: + pair_bias = self.pair_bias_proj(self.pair_norm(z)) + else: + pair_bias = z.unsqueeze(-1) + logits = logits + pair_bias.to(dtype=logits.dtype) - if attention_mask is not None: - min_val = torch.finfo(logits.dtype).min - mask_bias = torch.where( - attention_mask.bool()[:, None, :, None], 0.0, min_val - ) - logits = logits + mask_bias.to(dtype=logits.dtype) + if attention_mask is not None: + min_val = torch.finfo(logits.dtype).min + mask_bias = torch.where( + attention_mask.bool()[:, None, :, None], 0.0, min_val + ) + logits = logits + mask_bias.to(dtype=logits.dtype) - attn = torch.softmax(logits, dim=-2).to(dtype=v.dtype) - ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) - ctx = g * ctx - out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) + attn = torch.softmax(logits, dim=-2).to(dtype=v.dtype) + ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) + ctx = g * ctx + out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) if s is not None: out = torch.sigmoid(self.out_gate(s)) * out @@ -1235,10 +1069,6 @@ def __init__( ] ) - def set_kernel_backend(self, backend: str | None) -> None: - for attn in self.attn_blocks: - cast(AttentionPairBias, attn).set_kernel_backend(backend) - def forward( self, a: Tensor, @@ -1448,9 +1278,6 @@ def __init__( self.s_step_norm = nn.LayerNorm(c_token) self.token_norm = nn.LayerNorm(c_token) - def set_kernel_backend(self, backend: str | None) -> None: - self.token_transformer.set_kernel_backend(backend) - def forward( self, x_noisy: Tensor, @@ -1610,9 +1437,6 @@ def __init__(self, config: ESMFold2Config) -> None: self.inference_p = sh.inference_p self.inference_num_steps = sh.inference_num_steps - def set_kernel_backend(self, backend: str | None) -> None: - self.diffusion_module.set_kernel_backend(backend) - # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @@ -2304,7 +2128,6 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None ) self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) - self._use_kernels: bool = False # Default chunked for memory on long sequences; tests override with # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 # parity checks. @@ -2313,15 +2136,6 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size - def split_kernel_weights(self) -> tuple[Tensor, Tensor]: - return ( - self.proj_bundle.weight[: 2 * self.latent_channels, :], - self.proj_bundle.weight[2 * self.latent_channels :, :], - ) - - def _kernel_flow_direction(self) -> str: - return self.flow - def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: return torch.einsum(self._einsum_equation, left_stream, right_stream) @@ -2348,37 +2162,6 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor if visibility is None: visibility = pair_grid.new_ones(pair_grid.shape[:-1]) - if self._use_kernels: - p_in_weight, g_in_weight = self.split_kernel_weights() - - try: - return _cue_tri_mul( # type: ignore[misc] - pair_grid, - direction=self._kernel_flow_direction(), - mask=visibility, - norm_in_weight=self.norm_start.weight, - norm_in_bias=self.norm_start.bias, - p_in_weight=p_in_weight, - g_in_weight=g_in_weight, - norm_out_weight=self.norm_mix.weight, - norm_out_bias=self.norm_mix.bias, - p_out_weight=self.proj_emit.weight, - g_out_weight=self.proj_gate.weight, - eps=_EPS, - ) - except Exception as e: - import logging as _logging - - _logging.getLogger(__name__).warning( - "cuequivariance triangle_multiplicative_update kernel failed " - "(flow=%s, shape=%s, dtype=%s); falling back to chunked einsum. " - "Error: %s", - self.flow, - tuple(pair_grid.shape), - pair_grid.dtype, - e, - ) - normalized_grid = self.norm_start(pair_grid) bundled = self.proj_bundle(normalized_grid) signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) @@ -2407,15 +2190,6 @@ def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: input_channels=dim, latent_channels=dim, flow=flow ) - def set_kernel_backend(self, backend: str | None) -> None: - # Engine uses cueq when backend=="cuequivariance"; the "fused" backend - # routes through the parent PairUpdateBlock's fused path (bypassing this). - self._engine._use_kernels = backend == BACKEND_CUEQ - if backend == BACKEND_CUEQ and not CUE_AVAILABLE: - raise RuntimeError( - "backend='cuequivariance' but cuequivariance_torch is not installed." - ) - def set_chunk_size(self, chunk_size: int | None) -> None: self._engine.set_chunk_size(chunk_size) @@ -2437,88 +2211,11 @@ def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. self._chunk_size: int | None = 64 - self._fused_swiglu: nn.Module | None = None - self._kernel_backend: str | None = None def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size - def set_kernel_backend(self, backend: str | None) -> None: - """Install / uninstall FusedLNLinearSwiGLU (no cueq equivalent).""" - if backend not in _VALID_BACKENDS: - raise ValueError( - f"backend must be one of {_VALID_BACKENDS}, got {backend!r}" - ) - self._kernel_backend = backend - if backend == BACKEND_FUSED and TRITON_KERNELS_AVAILABLE: - assert _FusedLNLinearSwiGLU is not None - d_model = self.norm.normalized_shape[0] - d_inner = self.ffn.hidden_features - has_ln_bias = self.norm.bias is not None - device = self.ffn.w12.weight.device - dtype = self.ffn.w12.weight.dtype - fused = _FusedLNLinearSwiGLU( - d_model=d_model, - d_inner=d_inner, - has_ln_bias=has_ln_bias, - device=device, - dtype=dtype, - ) - with torch.no_grad(): - fused.LN_W.copy_(self.norm.weight) - if has_ln_bias: - fused.LN_B.copy_(self.norm.bias) # type: ignore[union-attr] - # FusedLNLinearSwiGLU.W12 is (d_model, 2*d_inner); transpose nn.Linear once. - fused.W12.copy_(self.ffn.w12.weight.t().contiguous()) - self._fused_swiglu = fused.eval().requires_grad_(False) - else: - self._fused_swiglu = None - - def _can_use_fused_path(self, x: Tensor) -> bool: - return ( - _fused_active(self, x) - and self._fused_swiglu is not None - and x.dtype == torch.bfloat16 - ) - - def _swiglu_pre_w3(self, x_normed: Tensor) -> Tensor: - """SwiGLU through silu(x1)*x2, before the final w3.""" - ffn = self.ffn - x12 = ffn.w12(x_normed) - x1, x2 = x12.split(ffn.hidden_features, dim=-1) - return F.silu(x1) * x2 - - def _addmm_residual(self, x: Tensor, hidden: Tensor) -> Tensor: - """x + w3(hidden) via single cuBLAS addmm — avoids transition-output allocation.""" - ffn = self.ffn - x_shape = x.shape - out = torch.addmm( - x.contiguous().view(-1, x_shape[-1]), - hidden.view(-1, hidden.shape[-1]), - ffn.w3.weight.t(), - ) - return out.view(x_shape) - def forward(self, x: Tensor) -> Tensor: - # Inference-only fast path (addmm-fused residual + pre-alloc out) - # — diverges bit-exactly from ``x + ffn(norm(x))`` so we only use - # it when grad is disabled (binder-design / bit-exact tests run - # with grad on and need the reference path). - if not torch.is_grad_enabled() and self._can_use_fused_path(x): - fused = self._fused_swiglu - assert fused is not None - pre_w3 = fused - if self._chunk_size is None or x.shape[1] <= self._chunk_size: - hidden = pre_w3(x) - return self._addmm_residual(x, hidden) - out = torch.empty_like(x) - for s in range(0, x.shape[1], self._chunk_size): - e = min(s + self._chunk_size, x.shape[1]) - sl = x[:, s:e] - hidden = pre_w3(sl) - out[:, s:e] = self._addmm_residual(sl, hidden) - return out - # Reference path — bit-exact with main: x + ffn(norm(x)). if self._chunk_size is None or x.shape[1] <= self._chunk_size: return x + self.ffn(self.norm(x)) out_list: list[Tensor] = [] @@ -2537,73 +2234,19 @@ def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) self.pair_transition = Transition(d_pair, expansion_ratio=expansion_ratio) - self._kernel_backend: str | None = None # Row-shared dropout-residual; r=0 for inference (HF model is inference-only). - # backend='fused' swaps in the FusedDropoutResidual Triton kernel. - self.row_drop = DropoutResidual(0.0, batch_dim=1, use_fused_kernels=False) - - def set_kernel_backend(self, backend: str | None) -> None: - if backend not in _VALID_BACKENDS: - raise ValueError( - f"backend must be one of {_VALID_BACKENDS}, got {backend!r}" - ) - self.tri_mul_out.set_kernel_backend(backend) - self.tri_mul_in.set_kernel_backend(backend) - self.pair_transition.set_kernel_backend(backend) - self._kernel_backend = backend - self.row_drop = DropoutResidual( - 0.0, batch_dim=1, use_fused_kernels=(backend == BACKEND_FUSED) - ) + self.row_drop = DropoutResidual(0.0, batch_dim=1) def set_chunk_size(self, chunk_size: int | None) -> None: self.tri_mul_out.set_chunk_size(chunk_size) self.tri_mul_in.set_chunk_size(chunk_size) self.pair_transition.set_chunk_size(chunk_size) - def _can_use_fused_trimul_with_residual(self, pair: Tensor) -> bool: - return _fused_active(self, pair) and pair.dtype == torch.bfloat16 - - def _fused_trimul_with_residual( - self, pair: Tensor, direction: str, pair_attention_mask: Tensor | None - ) -> Tensor: - """Fused TriMul+residual call; weights from the corresponding engine.""" - tri = self.tri_mul_out if direction == "outgoing" else self.tri_mul_in - engine: TriangleMultiplicativeBlock = tri._engine # type: ignore[assignment] - p_in_weight, g_in_weight = engine.split_kernel_weights() - - def _bf16(t: Tensor) -> Tensor: - return t if t.dtype == torch.bfloat16 else t.to(torch.bfloat16) - - return _fused_trimul_with_residual( # type: ignore[misc] - pair, - direction, - residual=pair, - drop_mask=None, # inference: no dropout, matches internal's eval path - norm_in_weight=_bf16(engine.norm_start.weight), - norm_in_bias=_bf16(engine.norm_start.bias), - p_in_weight=_bf16(p_in_weight), - g_in_weight=_bf16(g_in_weight), - norm_out_weight=_bf16(engine.norm_mix.weight), - norm_out_bias=_bf16(engine.norm_mix.bias), - p_out_weight=_bf16(engine.proj_emit.weight), - g_out_weight=_bf16(engine.proj_gate.weight), - mask=pair_attention_mask, - eps=_EPS, - ) - def forward( self, pair: Tensor, pair_attention_mask: Tensor | None = None ) -> Tensor: - if self._can_use_fused_trimul_with_residual(pair): - pair = self._fused_trimul_with_residual( - pair, "outgoing", pair_attention_mask - ) - pair = self._fused_trimul_with_residual( - pair, "incoming", pair_attention_mask - ) - else: - pair = self.row_drop(pair, self.tri_mul_out(pair, mask=pair_attention_mask)) - pair = self.row_drop(pair, self.tri_mul_in(pair, mask=pair_attention_mask)) + pair = self.row_drop(pair, self.tri_mul_out(pair, mask=pair_attention_mask)) + pair = self.row_drop(pair, self.tri_mul_in(pair, mask=pair_attention_mask)) pair = self.pair_transition(pair) return pair @@ -2622,10 +2265,6 @@ def __init__( ] ) - def set_kernel_backend(self, backend: str | None) -> None: - for block in self.blocks: - cast(PairUpdateBlock, block).set_kernel_backend(backend) - def set_chunk_size(self, chunk_size: int | None) -> None: for block in self.blocks: cast(PairUpdateBlock, block).set_chunk_size(chunk_size) @@ -2633,23 +2272,12 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def forward( self, pair: Tensor, pair_attention_mask: Tensor | None = None ) -> Tensor: - # Cast pair → bf16 internally when the fused trimul backend is enabled - # (its bwd kernel requires bf16). Other backends keep the input dtype. - orig_dtype = pair.dtype - fused_on = ( - len(self.blocks) > 0 - and getattr(self.blocks[0], "_kernel_backend", None) == BACKEND_FUSED - ) - if pair.is_cuda and fused_on and orig_dtype != torch.bfloat16: - pair = pair.to(torch.bfloat16) for block in self.blocks: fn = partial(block, pair_attention_mask=pair_attention_mask) if torch.is_grad_enabled(): pair = checkpoint(fn, pair, use_reentrant=False) # pyright: ignore else: pair = fn(pair) - if pair.dtype != orig_dtype: - pair = pair.to(orig_dtype) return pair diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py index 255229afbf66..8edc2b6b7b04 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py @@ -123,9 +123,6 @@ def __init__(self, config: ESMFold2Config) -> None: # Kernel / chunking configuration # ------------------------------------------------------------------ - def set_kernel_backend(self, backend: str | None) -> None: - self.folding_trunk.set_kernel_backend(backend) - def set_chunk_size(self, chunk_size: int | None) -> None: self.folding_trunk.set_chunk_size(chunk_size) @@ -572,13 +569,6 @@ def __init__(self, config: ESMFold2Config) -> None: self.post_init() - def set_kernel_backend(self, backend: str | None) -> None: - """Select kernel backend (None / "fused" / "cuequivariance").""" - self.folding_trunk.set_kernel_backend(backend) - if self.confidence_head is not None: - self.confidence_head.set_kernel_backend(backend) - self.structure_head.set_kernel_backend(backend) - def set_chunk_size(self, chunk_size: int | None) -> None: """Set chunk size for memory-efficient triangle multiplicative updates.""" self.folding_trunk.set_chunk_size(chunk_size) From 39f5331e80d8257a060dc67fbe62b3d102fda55a Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 17:09:44 +0100 Subject: [PATCH 03/70] Remove vendored distributed/ (2D context-parallel) stack from esmfold2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The distributed/ package is a NVIDIA/MIT-licensed 2D context-parallel implementation of the folding trunk (DTensor + DeviceMesh + NCCL) for multi-GPU 6B inference. It is dropped from the port because: - It is not imported by any model code (core, config, __init__, or the experimental file) — fully inert in the package. - It is broken on import: all 7 files import from `projects.huggingface.transformers.models.esmfold2...` (the fork's internal monorepo path), so `import transformers.models.esmfold2.distributed` raises ModuleNotFoundError. It never worked in the standalone layout. - It is NVIDIA/MIT-licensed, unlike the Apache/Biohub model code. - Transformers expresses parallelism declaratively via `base_model_tp_plan` / `tp_plan="auto"`, not a vendored per-model DTensor/NCCL stack. Nothing unique is lost: the math it shards already exists as the pure-PyTorch reference in modeling_esmfold2_common.py. If multi-GPU inference is needed later, author a tp_plan on ESMFold2Model fresh. Verified: nothing references distributed/; `import transformers` and the esmfold2 modeling module still import cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/distributed/__init__.py | 40 -- .../models/esmfold2/distributed/comm.py | 384 ------------ .../models/esmfold2/distributed/manager.py | 576 ------------------ .../esmfold2/distributed/model/__init__.py | 2 - .../distributed/model/layers/__init__.py | 2 - .../distributed/model/layers/layernorm.py | 205 ------- .../distributed/model/layers/linear.py | 199 ------ .../distributed/model/layers/pairformer.py | 181 ------ .../model/layers/triangular_mult.py | 427 ------------- .../models/esmfold2/distributed/utils.py | 477 --------------- 10 files changed, 2493 deletions(-) delete mode 100644 src/transformers/models/esmfold2/distributed/__init__.py delete mode 100644 src/transformers/models/esmfold2/distributed/comm.py delete mode 100644 src/transformers/models/esmfold2/distributed/manager.py delete mode 100644 src/transformers/models/esmfold2/distributed/model/__init__.py delete mode 100644 src/transformers/models/esmfold2/distributed/model/layers/__init__.py delete mode 100644 src/transformers/models/esmfold2/distributed/model/layers/layernorm.py delete mode 100644 src/transformers/models/esmfold2/distributed/model/layers/linear.py delete mode 100644 src/transformers/models/esmfold2/distributed/model/layers/pairformer.py delete mode 100644 src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py delete mode 100644 src/transformers/models/esmfold2/distributed/utils.py diff --git a/src/transformers/models/esmfold2/distributed/__init__.py b/src/transformers/models/esmfold2/distributed/__init__.py deleted file mode 100644 index 1854a7aa0afb..000000000000 --- a/src/transformers/models/esmfold2/distributed/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""2D context-parallel distributed extensions for ESMFold2.""" - -from projects.huggingface.transformers.models.esmfold2.distributed.manager import ( - DistributedManager, -) -from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.pairformer import ( - FoldingTrunkDistributed, -) -from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( - TrunkCPWrapper, - wrap_model_with_cp_trunks, -) - -__all__ = [ - "DistributedManager", - "FoldingTrunkDistributed", - "TrunkCPWrapper", - "wrap_model_with_cp_trunks", -] diff --git a/src/transformers/models/esmfold2/distributed/comm.py b/src/transformers/models/esmfold2/distributed/comm.py deleted file mode 100644 index 6a8f803ebb21..000000000000 --- a/src/transformers/models/esmfold2/distributed/comm.py +++ /dev/null @@ -1,384 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""Communication primitives for 2D context-parallel distributed operations.""" - -from typing import Optional - -import torch -import torch.distributed as dist - -from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( - LayoutMap, - get_group_rank_from_axial_shift, -) - - -class One2OneComm: - """Point-to-point communication with parity-based deadlock avoidance.""" - - def __init__( - self, - group: dist.ProcessGroup, - rank_send_to: int, - rank_recv_from: int, - parity: Optional[bool] = None, - ): - self.group = group - self.rank = dist.get_rank(self.group) - self.world_size = dist.get_world_size(self.group) - - if rank_send_to >= self.world_size: - raise ValueError(f"rank_send_to >= world_size {self.world_size}") - if rank_recv_from >= self.world_size: - raise ValueError(f"rank_recv_from >= world_size {self.world_size}") - - is_self_send = rank_send_to == self.rank - is_self_recv = rank_recv_from == self.rank - if is_self_send != is_self_recv: - raise ValueError( - "Asymmetric send/recv not supported: " - f"is_self_send={is_self_send}, is_self_recv={is_self_recv}" - ) - self.is_self_comm = is_self_send - self._rank_in_group_send_to = rank_send_to - self._rank_in_group_recv_from = rank_recv_from - self.parity = parity - - if not self.is_self_comm: - self.rank_send_to = dist.get_global_rank(self.group, rank_send_to) - self.rank_recv_from = dist.get_global_rank(self.group, rank_recv_from) - if self.parity is None: - self.parity = bool(self.rank % 2) - self._queue_send_recv = [] - self._work_to_finish = None - - def __deepcopy__(self, memo): - return One2OneComm( - self.group, - self._rank_in_group_send_to, - self._rank_in_group_recv_from, - self.parity, - ) - - def _prep_batch_isend_irecv( - self, to_send: torch.Tensor, to_recv: Optional[torch.Tensor] = None - ) -> torch.Tensor: - if self.is_self_comm: - if to_recv is None: - return to_send.detach().clone() - to_recv.copy_(to_send) - return to_recv - - ans = torch.empty_like(to_send) if to_recv is None else to_recv - if self.parity: - send_op = dist.P2POp( - dist.isend, to_send, self.rank_send_to, group=self.group - ) - recv_op = dist.P2POp(dist.irecv, ans, self.rank_recv_from, group=self.group) - self._queue_send_recv.append(send_op) - self._queue_send_recv.append(recv_op) - else: - recv_op = dist.P2POp(dist.irecv, ans, self.rank_recv_from, group=self.group) - send_op = dist.P2POp( - dist.isend, to_send, self.rank_send_to, group=self.group - ) - self._queue_send_recv.append(recv_op) - self._queue_send_recv.append(send_op) - return ans - - def _dispatch(self): - if self.is_self_comm: - return - if self._work_to_finish is not None: - raise RuntimeError("Unfinished communication in queue; cannot dispatch new") - self._work_to_finish = dist.batch_isend_irecv(self._queue_send_recv) - - def wait_until_finished(self): - if self.is_self_comm: - return - if self._work_to_finish is None: - raise RuntimeError("Cannot wait without unfinished communication") - for work in self._work_to_finish: - work.wait() - self._work_to_finish = None - self._queue_send_recv = [] - - def enqueue_to_dispatch( - self, to_send: torch.Tensor, to_recv: Optional[torch.Tensor] = None - ) -> torch.Tensor: - recv = self._prep_batch_isend_irecv(to_send, to_recv) - if self.is_self_comm: - return recv - self._dispatch() - return recv - - -class TransposeComm(One2OneComm): - """Transposes data between (i,j) and (j,i) on a square grid.""" - - def __init__(self, process_group: dist.ProcessGroup, group_layout: LayoutMap): - if group_layout.shape is None: - raise ValueError("group_layout must have a shape") - self.world_size = dist.get_world_size(process_group) - if self.world_size != group_layout.numel: - raise ValueError("Inconsistent world_size and group_layout.numel") - if len(group_layout.shape) != 2: - raise ValueError(f"{self.__class__} only supports 2D group layout") - if group_layout.shape[0] != group_layout.shape[1]: - raise ValueError(f"group_layout.shape {group_layout.shape} is not square") - - self.group_layout = group_layout - self.global_rank = dist.get_rank() - self.group_rank = dist.get_rank(process_group) - self.rank_coords: tuple[int, ...] = self.group_layout.unravel(self.group_rank) - - transpose_group_rank = self.group_layout(self.rank_coords[::-1]) - self.transpose_rank = dist.get_global_rank(process_group, transpose_group_rank) - self.parity_transpose = self.rank_coords[0] < self.rank_coords[1] - super().__init__( - process_group, - transpose_group_rank, - transpose_group_rank, - parity=self.parity_transpose, - ) - - def __deepcopy__(self, memo): - return TransposeComm(self.group, self.group_layout) - - -def ternary_parity(my_rank: int, send_rank: int, recv_rank: int) -> bool: - """Parity to avoid deadlocks: True if my_rank < min(send, recv).""" - return my_rank < min(send_rank, recv_rank) - - -class Ring2DComm: - """Ring communication on a 2D grid for TriangleMult and similar operations. - - Sets up: - - Transpose communication (send (i,j) to (j,i)) - - Row-wise ring (left shift per row) - - Column-wise ring (up shift per column) - - Initial offset shifts (row i shifts left by i; col j shifts up by j) - """ - - def __init__( - self, - group_2d: dist.ProcessGroup, - group_col: dist.ProcessGroup, - group_layout: LayoutMap, - ): - self.group_2d = group_2d - self.group_col = group_col - self.group_layout = group_layout - ranks_group_2d = set(dist.get_process_group_ranks(self.group_2d)) - ranks_group_col = set(dist.get_process_group_ranks(self.group_col)) - - if not ranks_group_col.issubset(ranks_group_2d): - raise ValueError("group_col ranks are not a subset of group_2d ranks") - - self.size_2d = dist.get_world_size(self.group_2d) - if self.size_2d != self.group_layout.numel: - raise ValueError( - f"group_2d size {self.size_2d} != group_layout.numel {self.group_layout.numel}" - ) - if self.group_layout.shape[0] != self.group_layout.shape[1]: - raise ValueError( - f"group_layout.shape {self.group_layout.shape} is not square" - ) - - self.rank_2d = dist.get_rank(self.group_2d) - self.coord_2d = self.group_layout.unravel(self.rank_2d) - - self.comm_2d_trans = TransposeComm(self.group_2d, self.group_layout) - - # Initial row shift: row i shifts left by i - self.send_rank_row_init = get_group_rank_from_axial_shift( - self.coord_2d, 1, -self.coord_2d[0], self.group_layout - ) - self.recv_rank_row_init = get_group_rank_from_axial_shift( - self.coord_2d, 1, self.coord_2d[0], self.group_layout - ) - self.comm_row_init = One2OneComm( - self.group_2d, - self.send_rank_row_init, - self.recv_rank_row_init, - parity=ternary_parity( - self.rank_2d, self.send_rank_row_init, self.recv_rank_row_init - ), - ) - - # Subsequent row shifts: left by 1 - self.send_rank_row = get_group_rank_from_axial_shift( - self.coord_2d, 1, -1, self.group_layout - ) - self.recv_rank_row = get_group_rank_from_axial_shift( - self.coord_2d, 1, 1, self.group_layout - ) - self.comm_row = One2OneComm( - self.group_2d, - self.send_rank_row, - self.recv_rank_row, - parity=ternary_parity(self.rank_2d, self.send_rank_row, self.recv_rank_row), - ) - - # Initial col shift: col j shifts up by j - self.send_rank_col_init = get_group_rank_from_axial_shift( - self.coord_2d, 0, -self.coord_2d[1], self.group_layout - ) - self.recv_rank_col_init = get_group_rank_from_axial_shift( - self.coord_2d, 0, self.coord_2d[1], self.group_layout - ) - self.comm_col_init = One2OneComm( - self.group_2d, - self.send_rank_col_init, - self.recv_rank_col_init, - parity=ternary_parity( - self.rank_2d, self.send_rank_col_init, self.recv_rank_col_init - ), - ) - - # Subsequent col shifts: up by 1 - self.send_rank_col = get_group_rank_from_axial_shift( - self.coord_2d, 0, -1, self.group_layout - ) - self.recv_rank_col = get_group_rank_from_axial_shift( - self.coord_2d, 0, 1, self.group_layout - ) - self.comm_col = One2OneComm( - self.group_2d, - self.send_rank_col, - self.recv_rank_col, - parity=ternary_parity(self.rank_2d, self.send_rank_col, self.recv_rank_col), - ) - - # Fused transpose + initial shift for backward pass - coords_t = self.coord_2d[::-1] - self.send_rank_transpose_row_init = get_group_rank_from_axial_shift( - coords_t, 1, -coords_t[0], self.group_layout - ) - recv_rank_transpose_row_init = get_group_rank_from_axial_shift( - self.coord_2d, 1, self.coord_2d[0], self.group_layout - ) - self.recv_rank_transpose_row_init = self.group_layout( - self.group_layout.unravel(recv_rank_transpose_row_init)[::-1] - ) - self.comm_transpose_row_init = One2OneComm( - self.group_2d, - self.send_rank_transpose_row_init, - self.recv_rank_transpose_row_init, - parity=ternary_parity( - self.rank_2d, - self.send_rank_transpose_row_init, - self.recv_rank_transpose_row_init, - ), - ) - - self.send_rank_transpose_col_init = get_group_rank_from_axial_shift( - coords_t, 0, -coords_t[1], self.group_layout - ) - recv_rank_transpose_col_init = get_group_rank_from_axial_shift( - self.coord_2d, 0, self.coord_2d[1], self.group_layout - ) - self.recv_rank_transpose_col_init = self.group_layout( - self.group_layout.unravel(recv_rank_transpose_col_init)[::-1] - ) - self.comm_transpose_col_init = One2OneComm( - self.group_2d, - self.send_rank_transpose_col_init, - self.recv_rank_transpose_col_init, - parity=ternary_parity( - self.rank_2d, - self.send_rank_transpose_col_init, - self.recv_rank_transpose_col_init, - ), - ) - - -class AttentionPairBiasComm: - """Communication setup for ring attention with pair bias. - - Manages transpose comms for K/V/mask and ring shift comms for K/V/Z. - """ - - def __init__( - self, - process_group: dist.ProcessGroup, - group_layout: LayoutMap, - cp_axis_0_group: dist.ProcessGroup, - cp_axis_1_group: dist.ProcessGroup, - ): - self.process_group = process_group - self.cp_axis_0_group = cp_axis_0_group - self.cp_axis_1_group = cp_axis_1_group - - if group_layout.shape is None: - raise ValueError("group_layout must have a shape") - self.world_size = dist.get_world_size(self.process_group) - if self.world_size != group_layout.numel: - raise ValueError("Inconsistent world_size and group_layout.numel") - if len(group_layout.shape) != 2: - raise ValueError(f"{self.__class__} only supports 2D group layout") - if group_layout.shape[0] != group_layout.shape[1]: - raise ValueError(f"group_layout.shape {group_layout.shape} is not square") - - self.group_layout = group_layout - self.global_rank = dist.get_rank() - self.group_rank = dist.get_rank(self.process_group) - self.rank_coords: tuple[int, ...] = self.group_layout.unravel(self.group_rank) - - self.comm_transpose_k = TransposeComm(self.process_group, self.group_layout) - self.comm_transpose_v = TransposeComm(self.process_group, self.group_layout) - self.comm_transpose_mask = TransposeComm(self.process_group, self.group_layout) - - self.send_rank_kvz = get_group_rank_from_axial_shift( - self.rank_coords, 1, 1, self.group_layout - ) - self.recv_rank_kvz = get_group_rank_from_axial_shift( - self.rank_coords, 1, -1, self.group_layout - ) - self.parity = self.rank_coords[1] % 2 == 1 - self.comm_k = One2OneComm( - self.process_group, - self.send_rank_kvz, - self.recv_rank_kvz, - parity=self.parity, - ) - self.comm_v = One2OneComm( - self.process_group, - self.send_rank_kvz, - self.recv_rank_kvz, - parity=self.parity, - ) - self.comm_z = One2OneComm( - self.process_group, - self.send_rank_kvz, - self.recv_rank_kvz, - parity=self.parity, - ) - - def __deepcopy__(self, memo): - return AttentionPairBiasComm( - self.process_group, - self.group_layout, - self.cp_axis_0_group, - self.cp_axis_1_group, - ) diff --git a/src/transformers/models/esmfold2/distributed/manager.py b/src/transformers/models/esmfold2/distributed/manager.py deleted file mode 100644 index f2b2c4b7e187..000000000000 --- a/src/transformers/models/esmfold2/distributed/manager.py +++ /dev/null @@ -1,576 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - - -import os -from copy import deepcopy -from math import prod -from typing import Any, Dict, Optional, OrderedDict, Union -from warnings import warn - -import torch - -from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( - LayoutMap, - LayoutRightMap, -) - -# grid_group_sizes objects must have -# (1) .values() attribute, (2) .items() attribute -_GridGroupSizesType = OrderedDict[str, Union[int, tuple[int, ...]]] - - -class DistributedManager: - """Borg-style singleton class for managing distributed state. - - Manages the device mesh, process groups, and subgroups for 2D context - parallelism. Initialized with e.g.:: - - DistributedManager.initialize(OrderedDict([("dp", 1), ("cp", (N, N))])) - - to create a 2D CP grid of size N×N. - """ - - # Borg-style shared state. Every instance's ``__dict__`` is rebound to - # ``_state`` in ``__new__``; one-time defaults are seeded via ``setdefault`` - # (rather than ``instance._foo = ...`` which pyright flags as "Cannot - # assign to attribute" on a class without those names declared). - _state: dict = {} - _DEFAULT_STATE: dict = { - "_initialized": False, - "_has_dist": False, - "_rank": 0, - "_world_size": 1, - "_local_rank": 0, - "_device": torch.device("cpu"), - "_backend": None, - "_device_mesh": None, - "_layout_device_mesh": None, - "_has_subgroups": False, - "_device_mesh_subgroups": None, - "_layout_device_mesh_subgroups": None, - "_group": {}, - "_group_rank": {}, - "_group_ranks": {}, - "_subgroups": {}, - "_subgroups_rank": {}, - "_subgroups_ranks": {}, - "_layout_subgroups": {}, - "_method_init": None, - } - - def __new__(cls): - instance = super().__new__(cls) - instance.__dict__ = cls._state - for key, default in cls._DEFAULT_STATE.items(): - cls._state.setdefault(key, default) - return instance - - @classmethod - def methods_init_available(cls) -> set[str]: - return {"ENV", "SLURM"} - - @classmethod - def backend_for_device(cls) -> Dict[str, Optional[str]]: - return { - "cuda": "nccl" if torch.distributed.is_nccl_available() else None, - "cpu": "gloo" if torch.distributed.is_gloo_available() else None, - } - - @classmethod - def is_initialized(cls) -> bool: - return cls._state.get("_initialized", False) - - def __init__(self): - if not self._initialized: - raise RuntimeError( - "A DistributedManager instance is being instantiated before " - "the singleton class is initialized. " - "Please call DistributedManager.initialize() first." - ) - super().__init__() - - def __getattr__(self, name: str) -> Any: - key_state = f"_{name}" - has_key_shared_state = key_state in self.__dict__ - has_key = name in self.__dict__ - if has_key_shared_state: - return self.__dict__[key_state] - elif has_key: - return self.__dict__[name] - else: - raise AttributeError(f'Attribute "{name}" or "_{name}" not found.') - - def __str__(self): - return ( - f"Initialized process {self.rank} of {self.world_size} using " - f"method '{self.method_init}'. Device set to {str(self.device)}. " - f"Backend is {self.backend}" - ) - - @staticmethod - def _setup( - grid_group_sizes: Optional[_GridGroupSizesType] = None, - device_type: str = "cuda", - backend: Optional[str] = None, - rank: int = -1, - node_rank: int = -1, - world_size: int = -1, - local_rank: Optional[int] = None, - addr: str = "localhost", - port: str = "29500", - method_init: str = "ENV", - **kwargs_init_pg, - ): - if device_type == "cuda" and not torch.cuda.is_available(): - raise RuntimeError( - f"Input device type {device_type} but torch.cuda is not available" - ) - - if world_size != -1 and grid_group_sizes is not None: - total_size = 1 - assert hasattr(grid_group_sizes, "values") - for value in grid_group_sizes.values(): - if isinstance(value, tuple) and all(isinstance(v, int) for v in value): - total_size *= prod(value) - elif isinstance(value, int): - total_size *= value - else: - raise RuntimeError( - f"Values in grid_group_sizes must be either int or tuple[int, ...], got {type(value)}" - ) - if world_size != total_size: - raise RuntimeError( - f"Non-default world_size {world_size} != product of grid_group_sizes values ({total_size})" - ) - - backend_for_device = DistributedManager.backend_for_device() - - if backend_for_device["cpu"] is None and backend_for_device["cuda"] is None: - raise RuntimeError( - f"No backend available for the supported device types: {backend_for_device.keys()}" - ) - - if device_type not in backend_for_device: - raise RuntimeError( - f"Invalid input device type {device_type}: only supports {backend_for_device.keys()}" - ) - - if backend is None: - backend = backend_for_device[device_type] - elif backend != backend_for_device[device_type]: - raise RuntimeError( - f"Invalid input backend {backend} for input device type {device_type}" - ) - - os.environ["MASTER_ADDR"] = addr - os.environ["MASTER_PORT"] = str(port) - - DistributedManager._state["_initialized"] = True - manager = DistributedManager() - - manager._has_dist = torch.distributed.is_available() # type: ignore[assignment] - manager._rank = rank # type: ignore[assignment] - manager._world_size = world_size # type: ignore[assignment] - manager._node_rank = node_rank # type: ignore[assignment] - - if device_type == "cuda": - if ( - manager.world_size > torch.cuda.device_count() - and manager.world_size % torch.cuda.device_count() - ): - warn("world_size is not a multiple of torch.cuda.device_count()") - if local_rank is None: - manager._local_rank = manager.rank % torch.cuda.device_count() # type: ignore[assignment] - else: - manager._local_rank = local_rank # type: ignore[assignment] - manager._device = torch.device(f"cuda:{manager.local_rank}") # type: ignore[assignment] - else: - if local_rank is not None: - manager._local_rank = local_rank # type: ignore[assignment] - manager._device = torch.device("cpu") # type: ignore[assignment] - - if not manager.has_dist: - warn("DistributedManager initialized without torch.distributed package") - return - - if manager.device.type == "cuda": - torch.cuda.set_device(manager.device) - torch.cuda.device(manager.device) - torch.cuda.empty_cache() - - manager._backend = backend # type: ignore[assignment] - - if manager.device.type == "cuda" and backend == "nccl": - try: - torch.distributed.init_process_group( - manager.backend, - rank=manager.rank, - world_size=manager.world_size, - device_id=manager.device, - **kwargs_init_pg, - ) - except TypeError: - torch.distributed.init_process_group( - manager.backend, - rank=manager.rank, - world_size=manager.world_size, - **kwargs_init_pg, - ) - else: - torch.distributed.init_process_group( - manager.backend, - rank=manager.rank, - world_size=manager.world_size, - **kwargs_init_pg, - ) - - manager._group["world"] = torch.distributed.group.WORLD - manager._group_rank["world"] = manager.rank - manager._group_ranks["world"] = torch.distributed.get_process_group_ranks( - manager.group["world"] - ) - manager._method_init = method_init # type: ignore[assignment] - - if grid_group_sizes is not None: - DistributedManager.create_grid_group(grid_group_sizes) - - @staticmethod - def _create_device_mesh_and_groups( - name: list[str], shape: list[int], suffix_mesh: Optional[str] = None - ) -> None: - if not DistributedManager.is_initialized(): - raise RuntimeError("DistributedManager is not initialized") - if ( - not DistributedManager._state["_has_dist"] - or not torch.distributed.is_available() - ): - raise RuntimeError( - "_create_device_mesh_and_groups requires torch.distributed" - ) - if ( - DistributedManager._state["_method_init"] is None - or DistributedManager._state["_method_init"] - not in DistributedManager.methods_init_available() - ): - raise RuntimeError( - f"Invalid DistributedManager method_init {DistributedManager._state['_method_init']}" - ) - if ( - DistributedManager._state["_backend"] is None - or DistributedManager._state["_backend"] - not in DistributedManager.backend_for_device().values() - ): - raise RuntimeError( - f"Invalid DistributedManager backend {DistributedManager._state['_backend']}" - ) - if ( - DistributedManager._state["_device"] is None - or DistributedManager._state["_device"].type - not in DistributedManager.backend_for_device().keys() - ): - raise RuntimeError( - f"Invalid DistributedManager device type {DistributedManager._state['_device'].type}" - ) - - world_size_expected = prod(shape) - if world_size_expected != DistributedManager._state["_world_size"]: - raise RuntimeError( - f"world_size {DistributedManager._state['_world_size']} does not match " - f"expected {world_size_expected} from shape {shape}" - ) - - device_type = DistributedManager._state["_device"].type - name_mesh = ( - f"_device_mesh_{suffix_mesh}" if suffix_mesh is not None else "_device_mesh" - ) - layout = LayoutRightMap(tuple(shape)) - DistributedManager._state[f"_layout{name_mesh}"] = layout - - grid2rank = torch.as_strided( - torch.arange(world_size_expected), size=layout.shape, stride=layout.strides - ) - DistributedManager._state[name_mesh] = torch.distributed.device_mesh.DeviceMesh( - device_type, grid2rank, mesh_dim_names=tuple(name) - ) - - for i_group in range(len(name)): - name_group = name[i_group] - if name_group in DistributedManager._state["_group"]: - continue - DistributedManager._state["_group"][name_group] = DistributedManager._state[ - name_mesh - ].get_group(name_group) - DistributedManager._state["_group_rank"][name_group] = ( - torch.distributed.get_group_rank( - DistributedManager._state["_group"][name_group], - DistributedManager._state["_rank"], - ) - ) - DistributedManager._state["_group_ranks"][name_group] = ( - torch.distributed.get_process_group_ranks( - DistributedManager._state["_group"][name_group] - ) - ) - - @staticmethod - def create_grid_group(grid_group_sizes: _GridGroupSizesType) -> None: - """Create a grid group for 2D context parallelism. - - Example:: - - from collections import OrderedDict - - DistributedManager.initialize(OrderedDict([("dp", 1), ("cp", (2, 2))])) - """ - shape_groups = [] - name_groups = [] - shape_subgroups = [] - name_subgroups = [] - group2subgroup = {} - group2subgroup_axes = {} - assert hasattr(grid_group_sizes, "items") - for k, v in grid_group_sizes.items(): - if isinstance(v, tuple) and all(isinstance(v_i, int) for v_i in v): - shape_groups.append(prod(v)) - name_groups.append(k) - shape_subgroups.extend(v) - names_this_subgroup = [f"{k}_axis_{i}" for i in range(len(v))] - name_subgroups.extend(names_this_subgroup) - group2subgroup[k] = names_this_subgroup - group2subgroup_axes[k] = list( - range(len(name_subgroups) - len(v), len(name_subgroups)) - ) - elif isinstance(v, int): - shape_groups.append(v) - name_groups.append(k) - shape_subgroups.append(v) - name_subgroups.append(k) - else: - raise RuntimeError( - f"Values in grid_group_sizes must be int or tuple[int, ...], got {type(v)}" - ) - - DistributedManager._create_device_mesh_and_groups(name_groups, shape_groups) - if (name_groups == name_subgroups) != (shape_groups == shape_subgroups): - raise RuntimeError("Inconsistent group and subgroup settings") - - DistributedManager._state["_has_subgroups"] = name_groups != name_subgroups - if DistributedManager._state["_has_subgroups"]: - if len(group2subgroup) == 0: - raise RuntimeError( - "group2subgroup is empty while _has_subgroups is True" - ) - DistributedManager._create_device_mesh_and_groups( - name_subgroups, shape_subgroups, suffix_mesh="subgroups" - ) - layout = DistributedManager._state["_layout_device_mesh_subgroups"] - coords = DistributedManager._state[ - "_device_mesh_subgroups" - ].get_coordinate() - for name_group, subgroup_names in group2subgroup.items(): - DistributedManager._state["_subgroups"][name_group] = [ - DistributedManager._state["_group"][n] for n in subgroup_names - ] - DistributedManager._state["_subgroups_ranks"][name_group] = [ - DistributedManager._state["_group_ranks"][n] for n in subgroup_names - ] - DistributedManager._state["_subgroups_rank"][name_group] = [ - DistributedManager._state["_group_rank"][n] for n in subgroup_names - ] - axes_subgroup = group2subgroup_axes[name_group] - slices = deepcopy(coords) - for axis in axes_subgroup: - slices[axis] = slice(None) - layout_subgroup = layout[tuple(slices)] - DistributedManager._state["_layout_subgroups"][name_group] = LayoutMap( - layout_subgroup.strides, layout_subgroup.shape, offset=0 - ) - - @staticmethod - def create_group(name: str, ranks: list[int], **kwargs_dist_ng) -> None: - DistributedManager._state["_group"][name] = torch.distributed.new_group( - ranks=ranks, **kwargs_dist_ng - ) - DistributedManager._state["_group_ranks"][name] = ranks - DistributedManager._state["_group_rank"][name] = ( - torch.distributed.get_group_rank( - DistributedManager._state["_group"][name], - DistributedManager._state["_rank"], - ) - ) - - @staticmethod - def _initialize_env(*args, **kwargs): - if not ("RANK" in os.environ and "WORLD_SIZE" in os.environ): - raise RuntimeError( - "environment variables RANK and WORLD_SIZE must be set for env:// initialization" - ) - rank = os.environ.get("RANK") - world_size = os.environ.get("WORLD_SIZE") - local_rank = os.environ.get("LOCAL_RANK") - group_rank = os.environ.get("GROUP_RANK", 0) - node_rank = int(os.environ.get("NODE_RANK", group_rank)) - try: - rank = int(rank) # type: ignore[arg-type] - world_size = int(world_size) # type: ignore[arg-type] - if local_rank is not None: - local_rank = int(local_rank) - except TypeError: - raise RuntimeError( - "environment variables RANK, LOCAL_RANK and WORLD_SIZE must be integers" - ) - DistributedManager._setup( - *args, - rank=rank, - node_rank=node_rank, - world_size=world_size, - local_rank=local_rank, - addr=os.environ.get("MASTER_ADDR"), # type: ignore[arg-type] - port=os.environ.get("MASTER_PORT"), # type: ignore[arg-type] - method_init="ENV", - **kwargs, - ) - - @staticmethod - def _initialize_slurm(*args, **kwargs): - keys = ( - "SLURM_PROCID", - "SLURM_NPROCS", - "SLURM_LOCALID", - "SLURM_LAUNCH_NODE_IPADDR", - ) - if not all(k in os.environ for k in keys): - raise RuntimeError( - f"environment variables {keys} must be set for SLURM initialization" - ) - rank = os.environ.get("SLURM_PROCID") - node_rank = int(os.environ.get("SLURM_NODEID", 0)) - world_size = os.environ.get("SLURM_NPROCS") - local_rank = os.environ.get("SLURM_LOCALID") - addr = os.environ.get("SLURM_LAUNCH_NODE_IPADDR") - try: - rank = int(rank) # type: ignore[arg-type] - world_size = int(world_size) # type: ignore[arg-type] - if local_rank is not None: - local_rank = int(local_rank) - except TypeError: - raise RuntimeError( - "environment variables SLURM_{PROCID,NPROCS,LOCALID} must be integers" - ) - DistributedManager._setup( - *args, - rank=rank, - node_rank=node_rank, - world_size=world_size, - local_rank=local_rank, - addr=addr, # type: ignore[arg-type] - method_init="SLURM", - **kwargs, - ) - - @staticmethod - def initialize( - grid_group_sizes: Optional[OrderedDict[str, int | tuple[int, ...]]] = None, - device_type: str = "cuda", - backend: Optional[str] = None, - **kwargs_init_pg, - ): - """Initialize the DistributedManager singleton. - - Parameters - ---------- - grid_group_sizes: - E.g. ``OrderedDict([("dp", 1), ("cp", (2, 2))])`` for a 2×2 CP grid. - device_type: - "cuda" (default) or "cpu". - backend: - Defaults to nccl for cuda, gloo for cpu. - """ - if DistributedManager.is_initialized(): - warn("DistributedManager is already initialized. Skip initialize()") - return - if backend == "nccl": - os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "0" - method_init = os.getenv("ESMCFOLD_DISTRIBUTED_INIT_METHOD") - if ( - method_init is not None - and method_init not in DistributedManager.methods_init_available() - ): - raise ValueError( - f"Unknown ESMCFOLD_DISTRIBUTED_INIT_METHOD={method_init}. " - f"Allowed: {DistributedManager.methods_init_available()}" - ) - if method_init is None: - try: - DistributedManager._initialize_env( - grid_group_sizes, - device_type=device_type, - backend=backend, - **kwargs_init_pg, - ) - except RuntimeError as except_env: - try: - DistributedManager._initialize_slurm( - grid_group_sizes, - device_type=device_type, - backend=backend, - **kwargs_init_pg, - ) - except RuntimeError as except_slurm: - warn( - "Could not initialize DistributedManager with env:// nor slurm.\n" - f"Error env://: {except_env}\n" - f"Error slurm: {except_slurm}\n" - "Will default initialize DistributedManager" - ) - DistributedManager._state["_initialized"] = True - elif method_init == "ENV": - DistributedManager._initialize_env( - grid_group_sizes, - device_type=device_type, - backend=backend, - **kwargs_init_pg, - ) - elif method_init == "SLURM": - DistributedManager._initialize_slurm( - grid_group_sizes, - device_type=device_type, - backend=backend, - **kwargs_init_pg, - ) - - @staticmethod - def cleanup(): - if DistributedManager._state.get("_group", {}) != {}: - if torch.distributed.is_initialized(): - if ( - DistributedManager._state["_device"].type == "cuda" - and torch.cuda.is_available() - ): - torch.distributed.barrier( - device_ids=[DistributedManager._state["_local_rank"]] - ) - else: - torch.distributed.barrier() - torch.distributed.destroy_process_group() - else: - DistributedManager._state = {} diff --git a/src/transformers/models/esmfold2/distributed/model/__init__.py b/src/transformers/models/esmfold2/distributed/model/__init__.py deleted file mode 100644 index eda8d5e6986f..000000000000 --- a/src/transformers/models/esmfold2/distributed/model/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT diff --git a/src/transformers/models/esmfold2/distributed/model/layers/__init__.py b/src/transformers/models/esmfold2/distributed/model/layers/__init__.py deleted file mode 100644 index eda8d5e6986f..000000000000 --- a/src/transformers/models/esmfold2/distributed/model/layers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT diff --git a/src/transformers/models/esmfold2/distributed/model/layers/layernorm.py b/src/transformers/models/esmfold2/distributed/model/layers/layernorm.py deleted file mode 100644 index 88fec07fa026..000000000000 --- a/src/transformers/models/esmfold2/distributed/model/layers/layernorm.py +++ /dev/null @@ -1,205 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""Distributed LayerNorm with parameters replicated across the device mesh.""" - -from typing import Optional, Union - -import torch -import torch.distributed as dist -import torch.nn as nn -import torch.nn.functional as F -from torch.distributed.tensor import DeviceMesh, DTensor, Replicate, distribute_tensor - -_shape_t = Union[int, list[int], torch.Size] - - -class _LayerNormParamsReplicatedImpl(torch.autograd.Function): - """LayerNorm with replicated parameters and arbitrary DTensor input placement.""" - - @staticmethod - def forward( - ctx, - x: DTensor, - normalized_shape: list[int], - weight: Optional[DTensor], - bias: Optional[DTensor], - eps: float, - reduce_group: dist.ProcessGroup, - ) -> DTensor: - if not isinstance(x, DTensor): - raise TypeError(f"x must be DTensor, got {type(x)}") - - x_local = x.to_local() - weight_local = weight.to_local() if weight is not None else None - bias_local = bias.to_local() if bias is not None else None - - ctx.reduce_group = reduce_group - ctx.eps = eps - ctx.normalized_shape = normalized_shape - ctx.x_shape = x.shape - ctx.x_stride = x.stride() - ctx.x_placements = x.placements - ctx.device_mesh = x.device_mesh - ctx.save_for_backward(x_local, weight_local) - - out_local = F.layer_norm( - x_local, normalized_shape, weight_local, bias_local, eps - ) - return DTensor.from_local( - out_local, - device_mesh=x.device_mesh, - placements=x.placements, - shape=x.shape, - stride=x.stride(), - ) - - @staticmethod - def backward(ctx, d_out: DTensor): - if not isinstance(d_out, DTensor): - raise TypeError(f"d_out must be DTensor, got {type(d_out)}") - - x_saved, weight_saved = ctx.saved_tensors - d_out_local = d_out.to_local() - eps = ctx.eps - normalized_shape = ctx.normalized_shape - - dx_dtensor: Optional[DTensor] = None - dw_dtensor: Optional[DTensor] = None - db_dtensor: Optional[DTensor] = None - - if ctx.needs_input_grad[0] or ctx.needs_input_grad[2]: - dims = tuple(-(i + 1) for i in range(len(normalized_shape))) - mean = x_saved.mean(dim=dims, keepdim=True) - var = x_saved.var(dim=dims, unbiased=False, keepdim=True) - x_norm = (x_saved - mean) / torch.sqrt(var + eps) - - if ctx.needs_input_grad[0]: - if weight_saved is not None: - dy = d_out_local * weight_saved.view( - *([1] * (d_out_local.ndim - len(normalized_shape))), - *weight_saved.shape, - ) - else: - dy = d_out_local - dims = tuple(-(i + 1) for i in range(len(normalized_shape))) - dy_mean = dy.mean(dim=dims, keepdim=True) - dy_x_norm_mean = (dy * x_norm).mean(dim=dims, keepdim=True) - dx_local = (dy - dy_mean - x_norm * dy_x_norm_mean) / torch.sqrt(var + eps) - dx_dtensor = DTensor.from_local( - dx_local, - device_mesh=ctx.device_mesh, - placements=ctx.x_placements, - shape=ctx.x_shape, - stride=ctx.x_stride, - ) - - if ctx.needs_input_grad[2]: - reduce_dims = list(range(d_out_local.ndim - len(normalized_shape))) - dw = (d_out_local * x_norm).sum(dim=reduce_dims).contiguous() - dw_work = dist.all_reduce( - dw, op=dist.ReduceOp.SUM, group=ctx.reduce_group, async_op=True - ) - - if ctx.needs_input_grad[3]: - reduce_dims = list(range(d_out_local.ndim - len(normalized_shape))) - db = d_out_local.sum(dim=reduce_dims).contiguous() - db_work = dist.all_reduce( - db, op=dist.ReduceOp.SUM, group=ctx.reduce_group, async_op=True - ) - - replicate = [Replicate()] * ctx.device_mesh.ndim - if ctx.needs_input_grad[2]: - dw_work.wait() # type: ignore[union-attr] - dw_dtensor = DTensor.from_local( - dw, - device_mesh=ctx.device_mesh, - placements=replicate, - shape=dw.shape, - stride=dw.stride(), - ) - if ctx.needs_input_grad[3]: - db_work.wait() # type: ignore[union-attr] - db_dtensor = DTensor.from_local( - db, - device_mesh=ctx.device_mesh, - placements=replicate, - shape=db.shape, - stride=db.stride(), - ) - - return dx_dtensor, None, dw_dtensor, db_dtensor, None, None - - -class LayerNormParamsReplicated(nn.Module): - """nn.LayerNorm wrapper with parameters replicated over the device mesh. - - Accepts DTensor inputs with arbitrary placements and outputs DTensors - with the same placements. - - Parameters - ---------- - layer_local: - The serial nn.LayerNorm layer whose parameters to replicate. - device_mesh: - The device mesh for distributing tensors. - """ - - def __init__(self, layer_local: nn.LayerNorm, device_mesh: DeviceMesh) -> None: - super().__init__() - if not isinstance(layer_local, nn.LayerNorm): - raise TypeError( - f"layer_local must be nn.LayerNorm, got {type(layer_local)}" - ) - - self.device_mesh = device_mesh - self.normalized_shape = list(layer_local.normalized_shape) - self.eps = layer_local.eps - replicate_placements = [Replicate()] * device_mesh.ndim - - if layer_local.weight is not None: - self.weight = nn.Parameter( - distribute_tensor(layer_local.weight, device_mesh, replicate_placements) - ) - else: - self.weight = None - - if layer_local.bias is not None: - self.bias = nn.Parameter( - distribute_tensor(layer_local.bias, device_mesh, replicate_placements) - ) - else: - self.bias = None - - if "cp" in device_mesh.mesh_dim_names: # type: ignore[operator] - self._reduce_group = device_mesh.get_group("cp") - else: - self._reduce_group = dist.group.WORLD - - def forward(self, x: DTensor) -> DTensor: - return _LayerNormParamsReplicatedImpl.apply( # type: ignore[return-value] - x, - self.normalized_shape, - self.weight, - self.bias, - self.eps, - self._reduce_group, - ) diff --git a/src/transformers/models/esmfold2/distributed/model/layers/linear.py b/src/transformers/models/esmfold2/distributed/model/layers/linear.py deleted file mode 100644 index ba195f0509a9..000000000000 --- a/src/transformers/models/esmfold2/distributed/model/layers/linear.py +++ /dev/null @@ -1,199 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""Distributed linear layer with parameters replicated across the device mesh.""" - -from typing import Optional - -import torch -import torch.distributed as dist -import torch.nn as nn -import torch.nn.functional as F -from torch import Tensor -from torch.distributed.device_mesh import DeviceMesh -from torch.distributed.tensor import DTensor, Replicate, distribute_tensor - -from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( - update_exhaustive_strides, -) - - -class _LinearParamsReplicatedImpl(torch.autograd.Function): - """Linear layer with replicated parameters and arbitrary input placements. - - Parameters are replicated on all mesh dimensions; inputs can be sharded - (e.g. Shard(0), Shard(1), Shard(2)) or replicated. Backward uses - all-reduce over the cp group to accumulate parameter gradients. - """ - - @staticmethod - def forward( - ctx, - x: DTensor, - weight: DTensor, - bias: Optional[DTensor], - reduce_group: dist.ProcessGroup, - avg_reduce: bool, - ) -> DTensor: - if not isinstance(x, DTensor): - raise TypeError(f"x must be DTensor, got {type(x)}") - if not isinstance(weight, DTensor): - raise TypeError(f"weight must be DTensor, got {type(weight)}") - - ctx.reduce_group = reduce_group - ctx.avg_reduce = avg_reduce - - x_local = x.to_local() - weight_local = weight.to_local() - bias_local = bias.to_local() if bias is not None else None - - ctx.save_for_backward(x_local, weight_local) - ctx.x_shape = x.shape - ctx.x_stride = x.stride() - ctx.x_placements = x.placements - ctx.device_mesh = x.device_mesh - - out_local = F.linear(x_local, weight_local, bias_local) - - shape_output = x.shape[:-1] + (weight.shape[0],) - stride_output = update_exhaustive_strides(x.shape, x.stride(), shape_output) - return DTensor.from_local( - out_local, - device_mesh=x.device_mesh, - placements=x.placements, - shape=shape_output, - stride=stride_output, - ) - - @staticmethod - def backward(ctx, d_out: DTensor): - if not isinstance(d_out, DTensor): - raise TypeError(f"d_out must be DTensor, got {type(d_out)}") - - x_saved, weight_saved = ctx.saved_tensors - d_out_local = d_out.to_local() - - dw: Optional[Tensor] = None - dw_work = None - if ctx.needs_input_grad[1]: - # Aggregate over all but the last two dims (batch + seq dims) - dw = torch.einsum("...i,...j->ij", d_out_local, x_saved) - dw = dw.contiguous() # type: ignore[union-attr] - op = dist.ReduceOp.AVG if ctx.avg_reduce else dist.ReduceOp.SUM - dw_work = dist.all_reduce(dw, op=op, group=ctx.reduce_group, async_op=True) - - db: Optional[Tensor] = None - db_work = None - if ctx.needs_input_grad[2]: - reduce_dims = list(range(d_out_local.ndim - 1)) - db = d_out_local.sum(dim=reduce_dims).contiguous() - op = dist.ReduceOp.AVG if ctx.avg_reduce else dist.ReduceOp.SUM - db_work = dist.all_reduce(db, op=op, group=ctx.reduce_group, async_op=True) - - dx_dtensor: Optional[DTensor] = None - if ctx.needs_input_grad[0]: - dx_local = F.linear(d_out_local, weight_saved.t()) - shape_dx = ctx.x_shape - stride_dx = ctx.x_stride - dx_dtensor = DTensor.from_local( - dx_local, - device_mesh=ctx.device_mesh, - placements=ctx.x_placements, - shape=shape_dx, - stride=stride_dx, - ) - - # Wrap parameter gradients as DTensors with Replicate placement - replicate = [Replicate()] * ctx.device_mesh.ndim - dw_dtensor: Optional[DTensor] = None - if dw_work is not None: - dw_work.wait() - dw_dtensor = DTensor.from_local( - dw, # type: ignore[arg-type] - device_mesh=ctx.device_mesh, - placements=replicate, - shape=dw.shape, # type: ignore[union-attr] - stride=dw.stride(), # type: ignore[union-attr] - ) - - db_dtensor: Optional[DTensor] = None - if db_work is not None: - db_work.wait() - db_dtensor = DTensor.from_local( - db, # type: ignore[arg-type] - device_mesh=ctx.device_mesh, - placements=replicate, - shape=db.shape, # type: ignore[union-attr] - stride=db.stride(), # type: ignore[union-attr] - ) - - return dx_dtensor, dw_dtensor, db_dtensor, None, None - - -class LinearParamsReplicated(nn.Module): - """nn.Linear wrapper with parameters replicated over the device mesh. - - Accepts DTensor inputs with arbitrary placements and outputs DTensors - with the same placements. Parameter gradients are all-reduced across - the CP group so every rank accumulates the full gradient. - - Parameters - ---------- - layer_local: - The serial nn.Linear layer whose parameters to replicate. - device_mesh: - The device mesh for distributing tensors. - avg_reduce: - If True, use AVG instead of SUM for all-reduce (useful when the - effective batch is already averaged). - """ - - def __init__( - self, layer_local: nn.Linear, device_mesh: DeviceMesh, avg_reduce: bool = False - ) -> None: - super().__init__() - if not isinstance(layer_local, nn.Linear): - raise TypeError(f"layer_local must be nn.Linear, got {type(layer_local)}") - - self.device_mesh = device_mesh - self.avg_reduce = avg_reduce - replicate_placements = [Replicate()] * device_mesh.ndim - - self.weight = nn.Parameter( - distribute_tensor(layer_local.weight, device_mesh, replicate_placements) - ) - if layer_local.bias is not None: - self.bias = nn.Parameter( - distribute_tensor(layer_local.bias, device_mesh, replicate_placements) - ) - else: - self.bias = None - - # Choose reduce group: use cp group if present, otherwise world - if "cp" in device_mesh.mesh_dim_names: # type: ignore[operator] - self._reduce_group = device_mesh.get_group("cp") - else: - self._reduce_group = dist.group.WORLD - - def forward(self, x: DTensor) -> DTensor: - return _LinearParamsReplicatedImpl.apply( # type: ignore[return-value] - x, self.weight, self.bias, self._reduce_group, self.avg_reduce - ) diff --git a/src/transformers/models/esmfold2/distributed/model/layers/pairformer.py b/src/transformers/models/esmfold2/distributed/model/layers/pairformer.py deleted file mode 100644 index 038f1471b1f2..000000000000 --- a/src/transformers/models/esmfold2/distributed/model/layers/pairformer.py +++ /dev/null @@ -1,181 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""Distributed PairUpdateBlock and Pairformer for ESMFold2. - -ESMFold2's Pairformer is pair-only (no single/sequence track), with each block: - PairUpdateBlock: - pair = pair + tri_mul_out(pair) - pair = pair + tri_mul_in(pair) - pair = pair_transition(pair) - -All three operations are distributed across the 2D CP grid. -""" - -from typing import Optional - -import torch.nn as nn -import torch.nn.functional as F -from torch.distributed.device_mesh import DeviceMesh -from torch.distributed.tensor import DTensor - -from projects.huggingface.transformers.models.esmfold2.distributed.comm import ( - Ring2DComm, -) -from projects.huggingface.transformers.models.esmfold2.distributed.manager import ( - DistributedManager, -) -from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.layernorm import ( - LayerNormParamsReplicated, -) -from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.linear import ( - LinearParamsReplicated, -) -from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.triangular_mult import ( - TriangleMultiplicativeBlockDistributed, -) -from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( - FoldingTrunk as SerialFoldingTrunk, -) -from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( - PairUpdateBlock as SerialPairUpdateBlock, -) -from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( - Transition as SerialTransition, -) - - -class TransitionDistributed(nn.Module): - """Distributed Transition block (LayerNorm + SwiGLU FFN). - - Parameters are replicated; the LayerNorm and both Linear layers - use DTensor with replicated params. - """ - - def __init__(self, layer: SerialTransition, device_mesh: DeviceMesh) -> None: - super().__init__() - if not isinstance(layer, SerialTransition): - raise TypeError(f"layer must be Transition, got {type(layer).__name__}") - self.norm = LayerNormParamsReplicated(layer.norm, device_mesh) - # SwiGLUMLP has w12 and w3 - self.w12 = LinearParamsReplicated(layer.ffn.w12, device_mesh) - self.w3 = LinearParamsReplicated(layer.ffn.w3, device_mesh) - self.hidden_features = layer.ffn.hidden_features - - def forward(self, x: DTensor) -> DTensor: - normed = self.norm(x) - x12 = self.w12(normed) - x1, x2 = x12.split(self.hidden_features, dim=-1) - hidden = F.silu(x1) * x2 - out = self.w3(hidden) - return x + out - - -class PairUpdateBlockDistributed(nn.Module): - """Distributed PairUpdateBlock. - - Computes: - pair = pair + tri_mul_out(pair, mask) - pair = pair + tri_mul_in(pair, mask) - pair = pair_transition(pair) - - All pair operations are distributed via 2D CP ring communication. - - Parameters - ---------- - layer: - Serial PairUpdateBlock to distribute. - dist_manager: - DistributedManager with the CP group and subgroups set up. - """ - - def __init__( - self, layer: SerialPairUpdateBlock, dist_manager: DistributedManager - ) -> None: - super().__init__() - if not isinstance(layer, SerialPairUpdateBlock): - raise TypeError( - f"layer must be PairUpdateBlock, got {type(layer).__name__}" - ) - - self.dist_manager = dist_manager - self.device_mesh = dist_manager.device_mesh_subgroups - - ring_comm_out = Ring2DComm( - dist_manager.group["cp"], - dist_manager.subgroups["cp"][0], - dist_manager.layout_subgroups["cp"], - ) - ring_comm_in = Ring2DComm( - dist_manager.group["cp"], - dist_manager.subgroups["cp"][0], - dist_manager.layout_subgroups["cp"], - ) - - self.tri_mul_out = TriangleMultiplicativeBlockDistributed( - layer.tri_mul_out._engine, self.device_mesh, ring_comm_out - ) - self.tri_mul_in = TriangleMultiplicativeBlockDistributed( - layer.tri_mul_in._engine, self.device_mesh, ring_comm_in - ) - self.pair_transition = TransitionDistributed( - layer.pair_transition, self.device_mesh - ) - - def forward( - self, pair: DTensor, pair_attention_mask: Optional[DTensor] = None - ) -> DTensor: - pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) - pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) - pair = self.pair_transition(pair) - return pair - - -class FoldingTrunkDistributed(nn.Module): - """Distributed Pairformer: ModuleList of PairUpdateBlockDistributed. - - Wraps the serial Pairformer by distributing each block. - - Parameters - ---------- - pairformer: - Serial Pairformer module. - dist_manager: - DistributedManager with the CP group and subgroups set up. - """ - - def __init__( - self, trunk: SerialFoldingTrunk, dist_manager: DistributedManager - ) -> None: - super().__init__() - if not isinstance(trunk, SerialFoldingTrunk): - raise TypeError(f"trunk must be FoldingTrunk, got {type(trunk).__name__}") - - self.blocks = nn.ModuleList( - [PairUpdateBlockDistributed(block, dist_manager) for block in trunk.blocks] # type: ignore[arg-type] - ) - - def forward( - self, pair: DTensor, pair_attention_mask: Optional[DTensor] = None - ) -> DTensor: - for block in self.blocks: - pair = block(pair, pair_attention_mask=pair_attention_mask) - return pair diff --git a/src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py b/src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py deleted file mode 100644 index da22c40b2710..000000000000 --- a/src/transformers/models/esmfold2/distributed/model/layers/triangular_mult.py +++ /dev/null @@ -1,427 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""Distributed TriangleMultiplicativeBlock for ESMFold2's pair representation. - -The pair tensor z has shape (B, N, N, d_pair) and is distributed across a 2D -CP grid as DTensor with placements (Shard(0), Shard(1), Shard(2)). GPU (i, j) -owns the shard z[..., i_start:i_end, j_start:j_end, :]. - -Triangle multiplication patterns: - Outgoing: contracted[b,n,m,d] = sum_k a[b,n,k,d] * b[b,m,k,d] (einsum "bnkd,bmkd->bnmd") - Incoming: contracted[b,n,m,d] = sum_k a[b,k,n,d] * b[b,k,m,d] (einsum "bknd,bkmd->bnmd") - -The distributed BMM uses ring communication to accumulate partial results: - - One operand is transposed across the 2D grid (so (i,j) gets the chunk from (j,i)) - - Both operands are ring-shifted (row-wise and column-wise respectively) - - Each step computes a local matmul; results are accumulated -""" - -from enum import Enum, auto -from typing import Tuple - -import torch -from torch import nn -from torch.distributed.device_mesh import DeviceMesh -from torch.distributed.tensor import DTensor, Shard - -from projects.huggingface.transformers.models.esmfold2.distributed.comm import ( - Ring2DComm, -) -from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.layernorm import ( - LayerNormParamsReplicated, -) -from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.linear import ( - LinearParamsReplicated, -) -from projects.huggingface.transformers.models.esmfold2.distributed.utils import ( - update_exhaustive_strides, -) -from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( - TriangleMultiplicativeBlock as SerialTriangleMultiplicativeBlock, -) - - -class _Direction(Enum): - Outgoing = auto() - Incoming = auto() - - -# --------------------------------------------------------------------------- -# Core distributed batch matmul -# --------------------------------------------------------------------------- - - -class _XposeArgs(Enum): - lhs = auto() - rhs = auto() - - -def _distributed_bmm( - lhs: torch.Tensor, - rhs: torch.Tensor, - comm: Ring2DComm, - permute_lhs: tuple[int, ...] | None = None, - permute_rhs: tuple[int, ...] | None = None, - permute_out: tuple[int, ...] | None = None, - xpose_args: _XposeArgs | None = None, -) -> torch.Tensor: - """Distributed batch matmul using ring communication on a 2D process grid. - - See boltz-cp's comm.py diagrams for the full algorithm description. - - Parameters - ---------- - lhs, rhs: - Local tensor shards. Shape: (B, ..., N, K) after optional permute. - comm: - Ring2DComm object providing all the communication handles. - permute_lhs, permute_rhs: - Optional permutations applied before computation. - permute_out: - Optional permutation applied to the output. - xpose_args: - Which operand to transpose across the grid first. - """ - if permute_lhs is not None: - lhs = lhs.permute(permute_lhs) - lhs = lhs.clone(memory_format=torch.contiguous_format) - if permute_rhs is not None: - rhs = rhs.permute(permute_rhs) - rhs = rhs.clone(memory_format=torch.contiguous_format) - - if xpose_args == _XposeArgs.lhs: - lhs_recv = comm.comm_2d_trans.enqueue_to_dispatch(lhs) - rhs_recv = rhs - rhs = torch.empty_like(rhs_recv) - elif xpose_args == _XposeArgs.rhs: - rhs_recv = comm.comm_2d_trans.enqueue_to_dispatch(rhs) - lhs_recv = lhs - lhs = torch.empty_like(lhs_recv) - elif xpose_args is None: - lhs_recv = lhs - lhs = torch.empty_like(lhs_recv) - rhs_recv = rhs - rhs = torch.empty_like(rhs_recv) - else: - raise ValueError(f"Invalid xpose_args: {xpose_args}") - - i_ready = 0 - i_recv = i_ready ^ 1 - lhs_buffer = [lhs_recv, lhs] - rhs_buffer = [rhs_recv, rhs] - - if xpose_args is not None: - comm.comm_2d_trans.wait_until_finished() - - lhs_buffer[i_recv] = comm.comm_row_init.enqueue_to_dispatch( - lhs_buffer[i_ready], lhs_buffer[i_recv] - ) - rhs_buffer[i_recv] = comm.comm_col_init.enqueue_to_dispatch( - rhs_buffer[i_ready], rhs_buffer[i_recv] - ) - - i_ready ^= 1 - i_recv ^= 1 - - out = torch.zeros_like(lhs_buffer[i_ready]) - - comm.comm_row_init.wait_until_finished() - comm.comm_col_init.wait_until_finished() - - for k_step in range(comm.group_layout.shape[1]): - lhs_ready = lhs_buffer[i_ready] - rhs_ready = rhs_buffer[i_ready] - if k_step < comm.group_layout.shape[1] - 1: - lhs_buffer[i_recv] = comm.comm_row.enqueue_to_dispatch( - lhs_ready, lhs_buffer[i_recv] - ) - rhs_buffer[i_recv] = comm.comm_col.enqueue_to_dispatch( - rhs_ready, rhs_buffer[i_recv] - ) - out = out + torch.matmul(lhs_ready, rhs_ready) - if k_step < comm.group_layout.shape[1] - 1: - comm.comm_row.wait_until_finished() - comm.comm_col.wait_until_finished() - i_ready ^= 1 - i_recv ^= 1 - - if permute_out is not None: - out = out.permute(permute_out) - return out - - -# --------------------------------------------------------------------------- -# Autograd function for distributed triangle multiplication -# --------------------------------------------------------------------------- - - -class _TriangleMultiplicativeBlockImpl(torch.autograd.Function): - """Distributed triangle multiplication autograd function. - - Forward - ------- - Input x has shape (B, N_local_row, N_local_col, 2*d) and is already: - x = signal * sigmoid(gate_logits) * visibility_mask (pre-combined inner gate + mask) - - The function splits x into a = x[..., :d] and b = x[..., d:], then computes - the triangle multiplication using ring communication. - - Backward - -------- - Propagates gradients back through the distributed BMM. - """ - - @staticmethod - @torch.amp.custom_fwd(device_type="cuda") - def forward(ctx, x: DTensor, comm: Ring2DComm, direction: _Direction) -> DTensor: - if not isinstance(x, DTensor): - raise TypeError(f"x must be DTensor, got {type(x)}") - if x.ndim != 4: - raise ValueError(f"x must be 4D, got {x.ndim}D") - if x.shape[-1] % 2 != 0: - raise ValueError(f"Last dim of x must be even, got {x.shape[-1]}") - placements = x.placements - if placements != (Shard(0), Shard(1), Shard(2)): - raise ValueError( - f"x must have placements (Shard(0), Shard(1), Shard(2)), got {placements}" - ) - - x_local = x.to_local() - a_local, b_local = torch.chunk(x_local, 2, dim=-1) - a_local = a_local.clone(memory_format=torch.contiguous_format) - b_local = b_local.clone(memory_format=torch.contiguous_format) - - if x.requires_grad: - ctx.save_for_backward(a_local, b_local) - ctx.comm = comm - ctx.shape_x = x.shape - ctx.stride_x = x.stride() - ctx.placements = placements - ctx.device_mesh = x.device_mesh - ctx.direction = direction - - if direction == _Direction.Outgoing: - # contracted[b,n,m,d] = sum_k a[b,n,k,d] * b[b,m,k,d] - # a: (B,n,k,d) → permute to (B,D,n,k) - # b: (B,m,k,d) → permute to (B,D,k,m) (needs transpose of (B,D,n,k)) - permute_lhs = (0, 3, 1, 2) - permute_rhs = (0, 3, 2, 1) - permute_out = (0, 2, 3, 1) - xpose_args = _XposeArgs.rhs - elif direction == _Direction.Incoming: - # contracted[b,n,m,d] = sum_k a[b,k,n,d] * b[b,k,m,d] - # a: (B,k,n,d) → permute to (B,D,n,k) => need (0,3,2,1) - # b: (B,k,m,d) → permute to (B,D,k,m) => need (0,3,1,2) - permute_lhs = (0, 3, 2, 1) - permute_rhs = (0, 3, 1, 2) - permute_out = (0, 2, 3, 1) - xpose_args = _XposeArgs.lhs - else: - raise ValueError(f"Invalid direction: {direction}") - - out_local = _distributed_bmm( - a_local, - b_local, - comm, - permute_lhs=permute_lhs, - permute_rhs=permute_rhs, - permute_out=permute_out, - xpose_args=xpose_args, - ).contiguous() - - # Output has shape (B, N_local_row, N_local_col, d) - shape_output = x.shape[:-1] + (out_local.shape[-1],) - stride_output = update_exhaustive_strides(x.shape, x.stride(), shape_output) - return DTensor.from_local( - out_local, - device_mesh=x.device_mesh, - placements=placements, - shape=shape_output, - stride=stride_output, - ) - - @staticmethod - @torch.amp.custom_bwd(device_type="cuda") - def backward(ctx, d_out: DTensor) -> Tuple[DTensor, None, None]: - if not isinstance(d_out, DTensor): - raise TypeError(f"d_out must be DTensor, got {type(d_out)}") - - a, b = ctx.saved_tensors - comm = ctx.comm - direction = ctx.direction - d_out_local = d_out.to_local().to(dtype=a.dtype) - - if direction == _Direction.Outgoing: - # d_a: d_out[b,n,m,d] * b[b,m,k,d] -> d_a[b,n,k,d] - # permute: d_out (B,n,m,D)->(B,D,n,m); b (B,m,k,D)->(B,D,m,k); out (B,D,n,k)->(B,n,k,D) - lhs_da, rhs_da = d_out_local, b - permute_lhs_da = (0, 3, 1, 2) - permute_rhs_da = (0, 3, 1, 2) - permute_out_da = (0, 2, 3, 1) - xpose_da = None - - # d_b: d_out[b,n,m,d] * a[b,n,k,d] -> d_b[b,m,k,d] - # permute: d_out (B,n,m,D)->(B,D,m,n); a (B,n,k,D)->(B,D,n,k); out (B,D,m,k)->(B,m,k,D) - lhs_db, rhs_db = d_out_local, a - permute_lhs_db = (0, 3, 2, 1) - permute_rhs_db = (0, 3, 1, 2) - permute_out_db = (0, 2, 3, 1) - xpose_db = _XposeArgs.lhs - - elif direction == _Direction.Incoming: - # d_a: d_out[b,n,m,d] * b[b,k,m,d] -> d_a[b,k,n,d] - lhs_da, rhs_da = b, d_out_local - permute_lhs_da = (0, 3, 1, 2) - permute_rhs_da = (0, 3, 2, 1) - permute_out_da = (0, 2, 3, 1) - xpose_da = _XposeArgs.rhs - - # d_b: d_out[b,n,m,d] * a[b,k,n,d] -> d_b[b,k,m,d] - lhs_db, rhs_db = a, d_out_local - permute_lhs_db = (0, 3, 1, 2) - permute_rhs_db = (0, 3, 1, 2) - permute_out_db = (0, 2, 3, 1) - xpose_db = None - else: - raise ValueError(f"Invalid direction: {direction}") - - da_local = _distributed_bmm( - lhs_da, - rhs_da, - comm, - permute_lhs=permute_lhs_da, - permute_rhs=permute_rhs_da, - permute_out=permute_out_da, - xpose_args=xpose_da, - ).contiguous() - db_local = _distributed_bmm( - lhs_db, - rhs_db, - comm, - permute_lhs=permute_lhs_db, - permute_rhs=permute_rhs_db, - permute_out=permute_out_db, - xpose_args=xpose_db, - ).contiguous() - - dab_local = torch.cat([da_local, db_local], dim=-1) - dx = DTensor.from_local( - dab_local, - device_mesh=ctx.device_mesh, - placements=ctx.placements, - shape=ctx.shape_x, - stride=ctx.stride_x, - ) - return dx, None, None - - -# --------------------------------------------------------------------------- -# Public distributed module -# --------------------------------------------------------------------------- - - -class TriangleMultiplicativeBlockDistributed(nn.Module): - """Distributed TriangleMultiplicativeBlock for ESMFold2's pair representation. - - Replaces the serial layer in a model by: - 1. Replacing all parameters with DTensor replicated parameters. - 2. Implementing the forward pass using distributed ring-communication BMM. - - The pair tensor z is expected as a DTensor with placements - (Shard(0), Shard(1), Shard(2)) on a 3D mesh (dp, cp_axis_0, cp_axis_1). - - Parameters - ---------- - layer: - The serial TriangleMultiplicativeBlock to distribute. - device_mesh: - The device mesh (should be the subgroups mesh: dp × cp_axis_0 × cp_axis_1). - comm: - Ring2DComm for the CP group. - """ - - def __init__( - self, - layer: SerialTriangleMultiplicativeBlock, - device_mesh: DeviceMesh, - comm: Ring2DComm, - ) -> None: - super().__init__() - if not isinstance(layer, SerialTriangleMultiplicativeBlock): - raise TypeError( - f"layer must be TriangleMultiplicativeBlock, got {type(layer).__name__}" - ) - self.device_mesh = device_mesh - self.ring_comm = comm - - self._direction = ( - _Direction.Outgoing if layer.flow == "outgoing" else _Direction.Incoming - ) - self._latent_channels = layer.latent_channels - - self.norm_start = LayerNormParamsReplicated(layer.norm_start, device_mesh) - self.norm_mix = LayerNormParamsReplicated(layer.norm_mix, device_mesh) - self.proj_bundle = LinearParamsReplicated(layer.proj_bundle, device_mesh) - self.proj_emit = LinearParamsReplicated(layer.proj_emit, device_mesh) - self.proj_gate = LinearParamsReplicated(layer.proj_gate, device_mesh) - - def forward(self, pair: DTensor, mask: DTensor | None = None) -> DTensor: - """Forward pass. - - Parameters - ---------- - pair: - Pair tensor (B, N, N, d_pair) as DTensor(Shard(0), Shard(1), Shard(2)). - mask: - Visibility mask (B, N, N) as DTensor(Shard(0), Shard(1), Shard(2)). - If None, no masking is applied. - - Returns - ------- - DTensor of same shape and placements as pair. - """ - # 1. Layer-normalise input - normalized = self.norm_start(pair) - - # 2. Compute bundled projection: (d → 4*d) - bundled = self.proj_bundle(normalized) - - # 3. Split into signal (2*d) and inner gate logits (2*d); apply inner gate - latent = self._latent_channels - signal, gate_logits = bundled.split(2 * latent, dim=-1) # DTensor ops - x = signal * gate_logits.sigmoid() - - # 4. Apply visibility mask - if mask is not None: - x = x * mask.unsqueeze(-1) - - # 5. Distributed triangle multiplication - contracted = _TriangleMultiplicativeBlockImpl.apply( - x, self.ring_comm, self._direction - ) - - # 6. Norm + output projection - out = self.proj_emit(self.norm_mix(contracted)) - - # 7. Output gate (applied to pre-norm input) - output_gate = self.proj_gate(normalized).sigmoid() - return out * output_gate diff --git a/src/transformers/models/esmfold2/distributed/utils.py b/src/transformers/models/esmfold2/distributed/utils.py deleted file mode 100644 index d2e1389c9dfb..000000000000 --- a/src/transformers/models/esmfold2/distributed/utils.py +++ /dev/null @@ -1,477 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -"""Layout / sharding helpers and the user-facing model-wrap entry point for -2D context parallelism.""" - -from math import isqrt, lcm -from typing import Sequence - -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch.distributed.tensor import Shard, distribute_tensor - - -class LayoutMap: - """Bijective mapping between multi-dimensional indices and flat indices. - - Analogous to C++ std::layout_stride::mapping. - - Parameters - ---------- - strides: - Per-dimension strides (must be positive integers). - shape: - Per-dimension sizes (must be positive integers). - offset: - Offset added to every flat index (default 0). - """ - - def __init__( - self, strides: tuple[int, ...], shape: tuple[int, ...], offset: int = 0 - ): - if not all(isinstance(s, (int, np.int64)) and s > 0 for s in strides): # type: ignore[arg-type] - raise ValueError(f"Strides must be positive integers: {strides}") - if any(s < 0 for s in shape): - raise ValueError(f"Shape must be non-negative: {shape}") - if any(s == 0 for s in shape): - raise ValueError(f"Shape must not contain zeros: {shape}") - - self._strides = strides - self._n_axes = len(strides) - if len(shape) != self._n_axes: - raise ValueError( - f"Shape {shape} and strides {strides} must have the same length" - ) - - self._shape = shape - self._numel = int(np.prod(self._shape)) - self._offset = offset - - shape_and_strides = np.array( - list(zip(self._shape, self._strides)), - dtype=np.dtype([("shape", int), ("strides", int)]), - ) - argsort_ascend = np.argsort(shape_and_strides, order=["strides", "shape"]) - - self.is_unique = self._is_unique(argsort_ascend) - self.is_exhaustive = self._is_exhaustive(argsort_ascend) - - if not self.is_unique: - raise ValueError( - f"Strides {strides} and shape {shape} do not give a unique layout." - ) - - self._required_span_size = self._compute_required_span_size() - self._argsort_descend_strides = argsort_ascend[::-1] - self._argsort_ascend_strides = argsort_ascend - - def _compute_required_span_size(self) -> int: - if self._n_axes == 0: - return 1 - return 1 + sum( - (self._shape[i] - 1) * self._strides[i] for i in range(self._n_axes) - ) - - def _strides_exhaustive(self, permutation: np.ndarray): - strides = np.array(self._strides) - shape = np.array(self._shape) - shape_permuted = shape[permutation] - strides_permuted = strides[permutation] - shape_shifted = np.concatenate([[1], shape_permuted[:-1]]) - strides_shifted = np.concatenate([[1], strides_permuted[:-1]]) - return strides_permuted, strides_shifted * shape_shifted - - def _is_unique(self, permutation: np.ndarray) -> bool: - if self._n_axes == 0: - return True - strides, strides_exhaustive = self._strides_exhaustive(permutation) - return bool(np.all(strides >= strides_exhaustive)) - - def _is_exhaustive(self, permutation: np.ndarray) -> bool: - if self._n_axes == 0: - return True - strides, strides_exhaustive = self._strides_exhaustive(permutation) - return bool(np.all(strides == strides_exhaustive)) - - @property - def offset(self) -> int: - return self._offset - - @property - def required_span_size(self) -> int: - return self._required_span_size - - @property - def numel(self) -> int: - return self._numel - - @property - def shape(self) -> tuple[int, ...]: - return self._shape - - @property - def strides(self) -> tuple[int, ...]: - return self._strides - - def __call__(self, ids: tuple[int, ...]) -> int: - if len(ids) != self._n_axes: - raise ValueError( - f"Expected {self._n_axes} elements in ids but got {len(ids)}" - ) - if len(ids) == 0: - return self._offset - if self._shape is not None: - for axis, idx in enumerate(ids): - if idx < 0 or idx >= self._shape[axis]: - raise ValueError( - f"ids[{axis}] == {idx} out of range [0, {self._shape[axis] - 1}]" - ) - return int(np.dot(ids, self._strides)) + self._offset - - def unravel(self, flat_index: int) -> tuple[int, ...]: - if not self.is_unique: - raise ValueError(f"Layout is not unique, cannot unravel {flat_index}") - if not isinstance(flat_index, (int, np.integer)): - raise TypeError(f"Expected int, got {type(flat_index)}") - remaining = flat_index - self._offset - if remaining < 0 or remaining >= self._required_span_size: - raise ValueError( - f"flat_index {flat_index} out of range [{self._offset}, " - f"{self._offset + self._required_span_size - 1}]" - ) - indices = [0] * self._n_axes - for i_dim in self._argsort_descend_strides: - stride = self._strides[i_dim] - size = self._shape[i_dim] - indices[i_dim] = (remaining // stride) % size - remaining -= indices[i_dim] * stride - if remaining != 0: - raise ValueError(f"flat_index {flat_index} is out of the valid span range.") - return tuple(indices) - - def __getitem__(self, slices) -> "LayoutMap": - if not isinstance(slices, tuple) and isinstance(slices, (slice, int)): - slices = (slices,) - if len(slices) < self._n_axes: - slices = slices + (slice(None),) * (self._n_axes - len(slices)) - - new_shape = [] - new_strides = [] - new_offset = self.offset - - for axis, s in enumerate(slices): - if isinstance(s, (int, np.int64)): # type: ignore[arg-type] - new_offset += s * self.strides[axis] # type: ignore[operator] - elif isinstance(s, slice): - start, stop, step = s.indices(self.shape[axis]) - if step <= 0: - raise ValueError("Unsupported slicing: negative or zero steps") - if start >= stop: - raise ValueError("Unsupported slicing: start not smaller than stop") - dim_len = max(0, (stop - start + step - 1) // step) - new_shape.append(dim_len) - new_strides.append(self.strides[axis] * step) - new_offset += start * self.strides[axis] - else: - raise TypeError(f"Unsupported slice type: {type(s)}") - - return LayoutMap(tuple(new_strides), tuple(new_shape), new_offset) - - -class LayoutRightMap(LayoutMap): - """Row-major (C-contiguous) layout.""" - - def __init__(self, shape: tuple[int, ...]): - strides = np.ones_like(shape) - strides[1:] = shape[:0:-1] - strides = np.cumprod(strides)[::-1] - super().__init__(tuple(strides), shape=shape) - - -class LayoutLeftMap(LayoutMap): - """Column-major (Fortran-contiguous) layout.""" - - def __init__(self, shape: tuple[int, ...]): - strides = np.ones_like(shape) - strides[1:] = shape[:-1] - strides = np.cumprod(strides) - super().__init__(tuple(strides), shape=shape) - - -def get_group_rank_from_axial_shift( - coord: tuple[int, ...], axis: int, delta: int, layout_group: LayoutMap -) -> int: - """Return the rank obtained by shifting coord along axis by delta (wrapping).""" - if len(coord) != len(layout_group.shape): - raise ValueError( - f"Incompatible coord {coord} and layout_group shape {layout_group.shape}" - ) - if axis >= len(coord): - raise ValueError(f"Axis {axis} out of range for coord {coord}") - coord_shifted = list(coord) - coord_shifted[axis] = (coord_shifted[axis] + delta) % layout_group.shape[axis] - return layout_group(coord_shifted) # type: ignore[arg-type] - - -def update_exhaustive_strides( - shape_original: Sequence[int], - strides_original: Sequence[int], - shape_new: Sequence[int], -) -> tuple[int, ...]: - """Compute strides for shape_new that preserve the same axis-ordering as - the exhaustive layout (shape_original, strides_original).""" - layout_original = LayoutMap(tuple(strides_original), tuple(shape_original)) - if not layout_original.is_exhaustive: - raise ValueError( - f"Layout (shape={shape_original}, strides={strides_original}) is not exhaustive" - ) - shape_new_ascending = np.array(shape_new)[layout_original._argsort_ascend_strides] - argsort_output = np.argsort(layout_original._argsort_ascend_strides) - strides_new_ascending = np.concatenate(([1], shape_new_ascending[:-1])).cumprod() - strides_new = strides_new_ascending[argsort_output] - return tuple(strides_new.tolist()) - - -def slice_repr_mask( - s: torch.Tensor, - z: torch.Tensor, - mask: torch.Tensor, - pair_mask: torch.Tensor, - n_ranks: int, - layout_group: LayoutMap, -) -> tuple[ - list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], list[torch.Tensor] -]: - """Slice s, z, mask, pair_mask into n_ranks shards for 2D CP distribution.""" - if z.shape[-2] != z.shape[-3]: - raise ValueError(f"z is not square in the middle two axes: {z.shape}") - if s.shape[-2] != z.shape[-3]: - raise ValueError(f"Incompatible s {s.shape} and z {z.shape}") - if mask.shape != s.shape[:-1]: - raise ValueError(f"Incompatible s {s.shape} and mask {mask.shape}") - if pair_mask.shape != z.shape[:-1]: - raise ValueError(f"Incompatible z {z.shape} and pair_mask {pair_mask.shape}") - - n_tokens = s.shape[-2] - coords = [layout_group.unravel(rank) for rank in range(n_ranks)] - n_ranks_axis = isqrt(n_ranks) - if n_ranks_axis * n_ranks_axis != n_ranks: - raise ValueError(f"n_ranks is not a perfect square: {n_ranks}") - if n_tokens % n_ranks_axis: - raise ValueError( - f"Token dim {n_tokens} not divisible by sqrt(n_ranks) = {n_ranks_axis}" - ) - stride = n_tokens // n_ranks_axis - s_slices, z_slices, mask_slices, pair_mask_slices = [], [], [], [] - for i_row, j_col in coords: - i0, i1 = i_row * stride, (i_row + 1) * stride - j0, j1 = j_col * stride, (j_col + 1) * stride - s_slices.append(s[..., i0:i1, :].contiguous()) - mask_slices.append(mask[..., i0:i1].contiguous()) - z_slices.append(z[..., i0:i1, j0:j1, :].contiguous()) - pair_mask_slices.append(pair_mask[..., i0:i1, j0:j1].contiguous()) - return s_slices, z_slices, mask_slices, pair_mask_slices - - -def tiled_softmax_attention_update( - o_chunk: torch.Tensor, - lse_m_chunk: torch.Tensor, - amax_chunk: torch.Tensor | None, - o: torch.Tensor | None = None, - lse_m: torch.Tensor | None = None, - amax: torch.Tensor | None = None, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]: - """Numerically-stable online softmax accumulation for ring attention. - - Updates running (o, lse_m, amax) with a new (o_chunk, lse_m_chunk, amax_chunk). - When amax_chunk is None the function operates without amax tracking. - """ - if not ((o is None) == (lse_m is None)): - raise ValueError("o and lse_m must both be None or both provided") - - has_amax = amax_chunk is not None - is_initial_chunk = o is None - - if lse_m_chunk.shape[-1] != 1: - raise ValueError("lse_m_chunk must have shape (..., 1)") - if o_chunk.ndim != lse_m_chunk.ndim: - raise ValueError("o_chunk and lse_m_chunk must have the same ndim") - if lse_m_chunk.shape[:-1] != o_chunk.shape[:-1]: - raise ValueError("o_chunk and lse_m_chunk must match except in the last dim") - - if is_initial_chunk: - return o_chunk, lse_m_chunk, amax_chunk - - if has_amax: - d_lse_m = lse_m - lse_m_chunk # type: ignore[operator] - amax_next = torch.maximum(amax_chunk, amax) # type: ignore[arg-type] - delta_lse = amax_chunk - amax - d_lse_m # type: ignore[operator] - o_new = o - torch.sigmoid(delta_lse) * (o - o_chunk) - lse_m_new = lse_m_chunk + torch.logsumexp( - torch.cat([(amax - amax_next) + d_lse_m, amax_chunk - amax_next], dim=-1), # type: ignore[operator] - dim=-1, - keepdim=True, - ).to(dtype=lse_m_chunk.dtype) - return o_new, lse_m_new, amax_next - else: - d_lse_m = lse_m - lse_m_chunk # type: ignore[operator] - o_new = o - torch.sigmoid(-d_lse_m) * (o - o_chunk) - lse_m_new = lse_m_chunk + torch.log1p(torch.exp(d_lse_m)).to( - dtype=lse_m_chunk.dtype - ) - return o_new, lse_m_new, None - - -# --------------------------------------------------------------------------- -# End-to-end CP runtime: drop-in replacement for the serial ``FoldingTrunk``. -# --------------------------------------------------------------------------- - - -class TrunkCPWrapper(nn.Module): - """Drop-in replacement for ``FoldingTrunk`` that runs distributed. - - Accepts and returns plain tensors so the rest of an ESMFold2 model - (LM, MSA encoder, diffusion sampler) is untouched. Pair tensors whose - ``N`` is not a multiple of the CP axes are zero-padded; the gathered - output is sliced back to the original length, and the mask is padded - the same way so padded rows/cols contribute nothing. - - Typical use on an ``N×N`` CP grid (e.g. 4 ranks via - ``torch.multiprocessing.spawn``):: - - from collections import OrderedDict - - from transformers.models.esmc import ESMFold2Model - from transformers.models.esmc.distributed import ( - DistributedManager, - wrap_model_with_cp_trunks, - ) - - DistributedManager.initialize(OrderedDict([("dp", 1), ("cp", (2, 2))])) - dm = DistributedManager() - - model = ESMFold2Model.from_pretrained(...).cuda().eval() - wrap_model_with_cp_trunks(model, dm) - # ``model.forward`` (and ``processor.fold``) now run the Pairformer - # across the CP grid; everything else stays serial per rank. - """ - - def __init__(self, serial_trunk: nn.Module, dist_manager) -> None: - super().__init__() - # Lazy imports: this module is imported by manager.py and the - # distributed layers, so importing pairformer / model_common at - # module level would create a cycle. - from projects.huggingface.transformers.models.esmfold2.distributed.manager import ( - DistributedManager, - ) - from projects.huggingface.transformers.models.esmfold2.distributed.model.layers.pairformer import ( - FoldingTrunkDistributed, - ) - from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( - FoldingTrunk as SerialFoldingTrunk, - ) - - if not isinstance(serial_trunk, SerialFoldingTrunk): - raise TypeError(f"expected FoldingTrunk, got {type(serial_trunk).__name__}") - if not isinstance(dist_manager, DistributedManager): - raise TypeError( - f"expected DistributedManager, got {type(dist_manager).__name__}" - ) - - # ``FoldingTrunkDistributed`` requires the serial trunk's chunking - # disabled (it composes its own ring loop). ``serial_trunk`` is typed - # ``nn.Module`` (the SerialFoldingTrunk symbol is imported lazily inside - # the function to avoid a circular import with pairformer.py); pyright - # can't narrow through the lazy-import isinstance check. - serial_trunk.set_chunk_size(None) # type: ignore[operator] - - self.dist_trunk = FoldingTrunkDistributed(serial_trunk, dist_manager) - self.dist_manager = dist_manager - self.device_mesh = dist_manager.device_mesh_subgroups - # device_mesh is (dp, cp_axis_0, cp_axis_1) - self.cp_axis_0 = self.device_mesh.size(1) - self.cp_axis_1 = self.device_mesh.size(2) - self.shard_factor = lcm(self.cp_axis_0, self.cp_axis_1) - - # The serial trunk exposes this knob to the parent model. The distributed - # path doesn't support chunking, but the parent ``set_chunk_size`` call - # still needs a no-op hook so it doesn't blow up. - def set_chunk_size(self, _chunk_size: int | None) -> None: - return - - def forward( - self, pair: torch.Tensor, pair_attention_mask: torch.Tensor | None = None - ) -> torch.Tensor: - N = pair.shape[1] - pad = (self.shard_factor - N % self.shard_factor) % self.shard_factor - if pad: - # F.pad pads from the last dim backward; pair is (B, N, N, d_pair). - pair = F.pad(pair, (0, 0, 0, pad, 0, pad)) - if pair_attention_mask is not None: - pair_attention_mask = F.pad(pair_attention_mask, (0, pad, 0, pad)) - - pair_dt = distribute_tensor( - pair.contiguous(), self.device_mesh, [Shard(0), Shard(1), Shard(2)] - ) - mask_dt = None - if pair_attention_mask is not None: - mask_dt = distribute_tensor( - pair_attention_mask.contiguous(), - self.device_mesh, - [Shard(0), Shard(1), Shard(2)], - ) - - out_dt = self.dist_trunk(pair_dt, pair_attention_mask=mask_dt) - out = out_dt.full_tensor() - if pad: - out = out[:, :N, :N, :] - return out - - -def wrap_model_with_cp_trunks(model: nn.Module, dist_manager) -> list[str]: - """Replace every ``FoldingTrunk`` submodule with ``TrunkCPWrapper``. - - Walks ``model.named_modules()`` and rebinds each attribute that points - at a serial ``FoldingTrunk``. Returns the list of replaced submodule - paths so callers (e.g. spawned workers) can log what got wrapped. - """ - from projects.huggingface.transformers.models.esmfold2.modeling_esmfold2_common import ( - FoldingTrunk as SerialFoldingTrunk, - ) - - targets: list[tuple[str, nn.Module, str, nn.Module]] = [] - for parent_name, parent in model.named_modules(): - for child_name, child in parent.named_children(): - if isinstance(child, SerialFoldingTrunk): - full = f"{parent_name}.{child_name}" if parent_name else child_name - targets.append((full, parent, child_name, child)) - - replaced: list[str] = [] - for full, parent, child_name, child in targets: - wrapped = TrunkCPWrapper(child, dist_manager).to( - device=dist_manager.device, dtype=next(child.parameters()).dtype - ) - setattr(parent, child_name, wrapped) - replaced.append(full) - return replaced From 88701d2cc0be1cdd7d08444d90e384315e1f919b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 17:25:37 +0100 Subject: [PATCH 04/70] Refactor ESMC attention onto the v5 ALL_ATTENTION_FUNCTIONS interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ESMC's bespoke attention dispatch with the standard v5 interface, mirroring models/esm (a bidirectional encoder). Behaviour is preserved bit-exactly at all real-token positions. Before: a hand-rolled `_scaled_dot_product_attention` choosing xformers -> flash-attn-2 -> SDPA, a `_FlashMultiHeadAttention` subclass with manual unpad_input/pad_input in ESMCModel.forward, a `_TritonRotaryEmbedding`, and a `seq_id`-threaded chain mask. After: - Module-level `eager_attention_forward`; `MultiHeadAttention` dispatches via `ALL_ATTENTION_FUNCTIONS.get_interface(config._attn_implementation, ...)` with q/k/v shaped (B, H, S, Dh) and `scaling=head_dim**-0.5` (RoPE is rotation-invariant to scaling, so it stays in the module). output_attentions forces the eager interface so probabilities remain observable. - ESMCModel.forward builds the 4D mask once: `create_bidirectional_mask` for the padding case (works for eager/sdpa/flash), and a block-diagonal additive bias for multi-chain `sequence_id` (eager/sdpa; flash multi-chain still raises). Removes the unpad/pad varlen path. - Drops the xformers / flash-attn / triton-rotary import machinery and their warnings. transformer_engine LN/MLP fusion is unchanged. The `_supports_sdpa/_supports_flash_attn/_supports_attention_backend` flags (already declared) are now actually honoured. flash-attn-2 is still supported — now via the standard attn_implementation backend rather than bespoke dispatch. Verified: loading identical weights into the refactored model reproduces the pre-refactor outputs to 0.000e+00 at every non-padding position (plain, padding-mask, and multi-chain cases); the only differences are at padding positions, which are masked out downstream. eager and sdpa agree bit-exactly on valid-token logits; MaskedLM/SequenceClassification/TokenClassification and output_attentions all work. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/modeling_esmc.py | 382 +++++------------- 1 file changed, 110 insertions(+), 272 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index f48a6ac51706..2f7d1cf7cb8c 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -16,24 +16,24 @@ import math import re from dataclasses import dataclass -from typing import Optional, cast +from typing import Callable, Optional, cast import torch import torch.nn as nn from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss from torch.nn import functional as F +from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] MaskedLMOutput, ModelOutput, SequenceClassifierOutput, TokenClassifierOutput, ) -from ...modeling_utils import PreTrainedModel # type: ignore[import] +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel # type: ignore[import] from ...utils import ( # type: ignore[import] auto_docstring, can_return_tuple, - is_flash_attn_2_available, logging, ) from .configuration_esmc import ESMCConfig @@ -43,24 +43,6 @@ _CONFIG_FOR_DOC = "ESMCConfig" -# Optional accelerated kernels. Pure-PyTorch fallbacks below if absent. -if is_flash_attn_2_available(): - from flash_attn import flash_attn_varlen_qkvpacked_func - from flash_attn.bert_padding import pad_input, unpad_input - - _flash_attn_available = True -else: - pad_input = unpad_input = flash_attn_varlen_qkvpacked_func = None - _flash_attn_available = False - -try: - from flash_attn.ops.triton.rotary import apply_rotary as apply_triton_rotary - - _flash_attn_rotary_available = torch.cuda.is_available() -except ImportError: - apply_triton_rotary = None # type: ignore[assignment] - _flash_attn_rotary_available = False - # Transformer Engine: fused LayerNorm+Linear / LayerNorm+MLP kernels with # fp32 reduction inside the LayerNorm. Recommended on GPU for accurate bf16 # inference; without it the pure-PyTorch fallback drifts ~O(10) in fp32 and @@ -74,25 +56,6 @@ te = None # type: ignore[assignment] _te_available = False -# xformers: preferred SDPA implementation on GPU. Provides a fused -# bf16 attention kernel with deterministic reduction order. Flash -# Attention 2 and PyTorch's ``F.scaled_dot_product_attention`` are -# progressively-less-preferred fallbacks. -try: - import xformers.ops as xops # type: ignore[import-untyped] - - _xformers_available = True -except ImportError: - xops = None # type: ignore[assignment] - _xformers_available = False - -# Flash Attention 2: secondary SDPA fallback. Used when xformers is not -# installed; fp16 / bf16 only. -if _flash_attn_available: - from flash_attn import flash_attn_func -else: - flash_attn_func = None # type: ignore[assignment] - if not _te_available: logger.warning( "ESMC: transformer_engine is not installed; falling back to " @@ -105,24 +68,6 @@ "reduction LayerNorm." ) -if not _xformers_available and not _flash_attn_available: - logger.warning( - "ESMC: neither xformers nor flash-attn is installed; falling back " - "to PyTorch ``F.scaled_dot_product_attention``. The attention " - "reduction order in bf16 differs from a fused kernel by ~1 bf16 " - "ULP per attention block; compounded across the 80-block stack " - "this reaches ~O(100) max-diff on the unnormalized residual stream. " - "Install xformers (preferred) with `pip install xformers` for a " - "fused attention kernel." - ) - -if torch.cuda.is_available() and not _flash_attn_rotary_available: - logger.warning( - "ESMC: flash-attn rotary kernel not installed; falling back to " - "pure-PyTorch RoPE. For faster GPU inference run `pip install flash-attn`." - ) - - # --------------------------------------------------------------------------- # Output dataclasses # --------------------------------------------------------------------------- @@ -421,50 +366,11 @@ def forward( cos = self._cos_cached[seqlen_offset:] sin = self._sin_cached[seqlen_offset:] - if _flash_attn_rotary_available and q.device.type == "cuda": - q_rot = apply_triton_rotary(q, cos, sin, interleaved=self.interleaved) # type: ignore[misc] - k_rot = apply_triton_rotary(k, cos, sin, interleaved=self.interleaved) # type: ignore[misc] - else: - q_rot = _apply_rotary_emb_torch(q, cos, sin, self.interleaved) - k_rot = _apply_rotary_emb_torch(k, cos, sin, self.interleaved) + q_rot = _apply_rotary_emb_torch(q, cos, sin, self.interleaved) + k_rot = _apply_rotary_emb_torch(k, cos, sin, self.interleaved) return q_rot, k_rot -class _TritonRotaryEmbedding(RotaryEmbedding): - """RoPE variant that delegates to the Flash-Attention Triton kernel. - - Only used inside :class:`_FlashMultiHeadAttention` when Flash Attention 2 - is available. The ``forward`` signature differs from :class:`RotaryEmbedding` - because Flash Attention packs Q, K, V together. - """ - - def forward( - self, qkv: torch.Tensor, cu_seqlens: torch.Tensor, max_seqlen: int - ) -> torch.Tensor: # type: ignore[override] - """Apply RoPE in-place to a packed ``(N, 3, n_heads, head_dim)`` tensor.""" - self._update_cos_sin_cache(max_seqlen, device=qkv.device, dtype=qkv.dtype) - assert self._cos_cached is not None and self._sin_cached is not None - assert apply_triton_rotary is not None - - apply_triton_rotary( - qkv[:, 0], - self._cos_cached, - self._sin_cached, - cu_seqlens=cu_seqlens, - max_seqlen=max_seqlen, - inplace=True, - ) - apply_triton_rotary( - qkv[:, 1], - self._cos_cached, - self._sin_cached, - cu_seqlens=cu_seqlens, - max_seqlen=max_seqlen, - inplace=True, - ) - return qkv - - # --------------------------------------------------------------------------- # Feed-forward network helpers # --------------------------------------------------------------------------- @@ -594,60 +500,33 @@ def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequent # --------------------------------------------------------------------------- -def _scaled_dot_product_attention( - q: torch.Tensor, - k: torch.Tensor, - v: torch.Tensor, - *, - n_heads: int, - d_head: int, - seq_id: torch.Tensor | None, -) -> torch.Tensor: - """Scaled dot-product attention with optional chain-aware mask. - - Dispatches in order of preference: - 1. xformers ``memory_efficient_attention`` — preferred fused kernel, - requires ``xformers``, no chain mask. - 2. Flash Attention 2 (``flash_attn.flash_attn_func``) — secondary - fused kernel, requires ``flash-attn``, no chain mask, fp16 / - bf16 only. - 3. PyTorch's ``F.scaled_dot_product_attention`` — last-resort path; - also handles the chain-aware mask when ``seq_id`` is present - and the fp32 path that Flash Attention 2 does not support. +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: torch.Tensor | None, + scaling: float | None = None, + dropout: float = 0.0, + **kwargs, +) -> tuple[torch.Tensor, torch.Tensor]: + """Reference attention ``softmax(Q @ Kᵀ * scaling + mask) @ V``. + + Operates on ``(batch, n_heads, seq_len, head_dim)`` query/key/value and + returns ``(attn_output, attn_weights)``; ``attn_output`` is transposed back + to ``(batch, seq_len, n_heads, head_dim)`` to match the other + ``ALL_ATTENTION_FUNCTIONS`` backends. """ - if seq_id is None and _xformers_available: - b, s, _ = q.shape - q4 = q.view(b, s, n_heads, d_head) - k4 = k.view(b, s, n_heads, d_head) - v4 = v.view(b, s, n_heads, d_head) - context = xops.memory_efficient_attention( # type: ignore[union-attr] - q4, k4, v4, attn_bias=None, scale=d_head**-0.5 - ) - return context.reshape(b, s, n_heads * d_head) - if ( - seq_id is None - and _flash_attn_available - and q.dtype in (torch.float16, torch.bfloat16) - ): - b, s, _ = q.shape - q4 = q.view(b, s, n_heads, d_head) - k4 = k.view(b, s, n_heads, d_head) - v4 = v.view(b, s, n_heads, d_head) - context = flash_attn_func( # type: ignore[misc] - q4, k4, v4, dropout_p=0.0, softmax_scale=d_head**-0.5 - ) - return context.reshape(b, s, n_heads * d_head) # type: ignore[union-attr] - b, s, _ = q.shape - q = q.view(b, s, n_heads, -1).transpose(1, 2) - k = k.view(b, s, n_heads, -1).transpose(1, 2) - v = v.view(b, s, n_heads, -1).transpose(1, 2) - if seq_id is not None: - mask = (seq_id.unsqueeze(-1) == seq_id.unsqueeze(-2)).unsqueeze(1) - context = F.scaled_dot_product_attention(q, k, v, mask) - else: - context = F.scaled_dot_product_attention(q, k, v) - _, h, _, d_out = context.shape - return context.transpose(1, 2).reshape(b, s, h * d_out) + if scaling is None: + scaling = query.size(-1) ** -0.5 + attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling + if attention_mask is not None: + attn_weights = attn_weights + attention_mask[:, :, :, : key.shape[-2]] + attn_weights = nn.functional.softmax(attn_weights, dim=-1) + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) + attn_output = torch.matmul(attn_weights, value) + attn_output = attn_output.transpose(1, 2).contiguous() + return attn_output, attn_weights class MultiHeadAttention(nn.Module): @@ -662,12 +541,20 @@ class MultiHeadAttention(nn.Module): """ def __init__( - self, d_model: int, n_heads: int, bias: bool = False, qk_layernorm: bool = True + self, + config: ESMCConfig, + d_model: int, + n_heads: int, + bias: bool = False, + qk_layernorm: bool = True, ): super().__init__() + self.config = config self.d_model = d_model self.n_heads = n_heads self.d_head = d_model // n_heads + self.scaling = self.d_head**-0.5 + self.attention_dropout = 0.0 assert not bias, "ESMC was trained with bias=False; bias=True not supported" self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) @@ -694,95 +581,50 @@ def _apply_rotary( def forward( self, - x: torch.Tensor, - seq_id: torch.Tensor | None, + hidden_states: torch.Tensor, + attention_mask: torch.Tensor | None = None, output_attentions: bool = False, + **kwargs, ) -> tuple[torch.Tensor, torch.Tensor | None]: """Return ``(context, attn_weights)``. - ``attn_weights`` is ``None`` unless ``output_attentions=True`` — the - fused SDPA backends (xformers, flash-attn 2, ``F.scaled_dot_product_attention``) - don't expose attention probabilities, so capturing them forces a - materialized ``softmax(Q @ K.T / sqrt(d)) @ V`` path with shape - ``(B, H, L, L)``. + Attention is computed by the backend selected through + ``config._attn_implementation`` (``eager`` / ``sdpa`` / + ``flash_attention_2`` / ...). RoPE and QK-LayerNorm are applied here; the + backend only computes ``softmax(QKᵀ)V``. ``attn_weights`` is ``None`` + unless the backend exposes the probabilities — ``output_attentions=True`` + forces the ``eager`` interface so they are observable. """ - qkv = self.layernorm_qkv(x) + b, s, _ = hidden_states.shape + qkv = self.layernorm_qkv(hidden_states) q, k, v = torch.chunk(qkv, 3, dim=-1) q = self.q_ln(q).to(q.dtype) k = self.k_ln(k).to(q.dtype) q, k = self._apply_rotary(q, k) - b, s, _ = q.shape - - if output_attentions: - # Manual SDPA so attention probabilities are observable. - q4 = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - k4 = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - v4 = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - scale = self.d_head**-0.5 - attn_scores = (q4 @ k4.transpose(-2, -1)) * scale - if seq_id is not None: - mask = (seq_id.unsqueeze(-1) == seq_id.unsqueeze(-2)).unsqueeze(1) - attn_scores = attn_scores.masked_fill(~mask, float("-inf")) - attn_weights = torch.softmax(attn_scores, dim=-1) - context = (attn_weights @ v4).transpose(1, 2).reshape(b, s, -1) - return self.out_proj(context), attn_weights - - context = _scaled_dot_product_attention( - q, k, v, n_heads=self.n_heads, d_head=self.d_head, seq_id=seq_id - ) - return self.out_proj(context), None - - -class _FlashMultiHeadAttention(MultiHeadAttention): - """Flash-Attention 2 variant of :class:`MultiHeadAttention`.""" + # (B, S, D) -> (B, H, S, Dh) + q = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + k = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + v = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - def __init__( - self, d_model: int, n_heads: int, bias: bool = False, qk_layernorm: bool = True - ): - super().__init__( - d_model=d_model, n_heads=n_heads, bias=bias, qk_layernorm=qk_layernorm - ) - self.rotary = _TritonRotaryEmbedding(d_model // n_heads) - - def forward( - self, - x: torch.Tensor, - seq_id: torch.Tensor | None, - output_attentions: bool = False, - ) -> tuple[torch.Tensor, torch.Tensor | None]: - if output_attentions: - raise ValueError( - "output_attentions=True is not supported with " - "attn_implementation='flash_attention_2'. " - "Re-load the model with attn_implementation='sdpa' (or 'eager')." + attention_interface: Callable = eager_attention_forward + if not output_attentions: + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward ) - assert seq_id is not None and seq_id.dtype == torch.bool - - seqlens = seq_id.sum(dim=-1, dtype=torch.int32) - cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) - max_seqlen = int(seqlens.max().item()) - - qkv = self.layernorm_qkv(x) - q, k, v = torch.chunk(qkv, 3, dim=-1) - q = self.q_ln(q).to(q.dtype) - k = self.k_ln(k).to(q.dtype) - - # ``q``/``k``/``v`` are 2D ``(T, D)`` here: the parent ``ESMCModel.forward`` - # calls ``unpad_input`` before the transformer stack to produce the - # varlen-flat layout that ``flash_attn_varlen_qkvpacked_func`` requires. - T = q.shape[0] - qkv_packed = torch.stack([q, k, v], dim=1).view(T, 3, self.n_heads, self.d_head) - qkv_packed = self.rotary(qkv_packed, cu_seqlens, max_seqlen) - context = flash_attn_varlen_qkvpacked_func( # type: ignore[misc] - qkv_packed, cu_seqlens, max_seqlen, softmax_scale=self.d_head**-0.5 - ) - n_out, h_out, d_out = context.shape # type: ignore[union-attr] - return ( - self.out_proj(context.reshape(n_out, h_out * d_out)), # type: ignore[union-attr] - None, + attn_output, attn_weights = attention_interface( + self, + q, + k, + v, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, ) + attn_output = attn_output.reshape(b, s, -1) + return self.out_proj(attn_output), attn_weights # --------------------------------------------------------------------------- @@ -807,9 +649,9 @@ class UnifiedTransformerBlock(nn.Module): def __init__( self, + config: ESMCConfig, d_model: int, n_heads: int, - use_flash_attn: bool = False, bias: bool = False, expansion_ratio: float = 4.0, residue_scaling_factor: float = 1.0, @@ -818,8 +660,9 @@ def __init__( ): super().__init__() - attn_cls = _FlashMultiHeadAttention if use_flash_attn else MultiHeadAttention - self.attn = attn_cls(d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + self.attn = MultiHeadAttention( + config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm + ) if ffn_type == "swiglu": self.ffn = _swiglu_ln_ffn(d_model, expansion_ratio, bias) @@ -835,17 +678,16 @@ def __init__( def forward( self, x: torch.Tensor, - sequence_id: torch.Tensor | None, + attention_mask: torch.Tensor | None = None, output_attentions: bool = False, + **kwargs, ) -> tuple[torch.Tensor, torch.Tensor | None]: """ Args: x: ``(batch, seq_len, d_model)`` - sequence_id: ``(batch, seq_len)`` chain-ID tensor used to restrict - attention to tokens within the same chain. SDPA blocks accept - an integer tensor (``-1`` marks padding); the flash-attn block - takes a ``bool`` padding mask — the caller selects which. - ``None`` skips chain-aware masking entirely (fast path). + attention_mask: Additive attention bias broadcastable to + ``(batch, num_heads, seq_len, seq_len)``, or ``None`` for full + (unmasked) attention. output_attentions: When ``True``, returns the per-head attention weights for this block alongside the residual output. @@ -855,7 +697,7 @@ def forward( ``(batch, num_heads, seq_len, seq_len)`` or ``None``. """ attn_out, attn_weights = self.attn( - x, sequence_id, output_attentions=output_attentions + x, attention_mask, output_attentions=output_attentions, **kwargs ) x = x + attn_out / self.scaling_factor x = x + self.ffn(x) / self.scaling_factor @@ -880,6 +722,7 @@ class TransformerStack(nn.Module): def __init__( self, + config: ESMCConfig, d_model: int, n_heads: int, n_layers: int, @@ -888,15 +731,14 @@ def __init__( qk_layernorm: bool = True, ffn_type: str = "swiglu", expansion_ratio: float = 8 / 3, - use_flash_attn: bool = False, ): super().__init__() self.blocks = nn.ModuleList( [ UnifiedTransformerBlock( + config, d_model, n_heads, - use_flash_attn=use_flash_attn, residue_scaling_factor=math.sqrt(n_layers / 36) if scale_residue else 1.0, @@ -913,9 +755,10 @@ def __init__( def forward( self, x: torch.Tensor, - sequence_id: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, layers_to_collect: list[int] | None = None, output_attentions: bool = False, + **kwargs, ) -> tuple[ torch.Tensor, torch.Tensor, @@ -926,7 +769,8 @@ def forward( Args: x: ``(batch, seq_len, d_model)`` - sequence_id: Optional chain-id tensor forwarded to each block. + attention_mask: Additive attention bias forwarded to each block, or + ``None`` for full attention. layers_to_collect: Layer indices (0-based pre-block inputs plus ``n_layers`` for the post-norm output) whose hidden states should be returned. @@ -947,7 +791,9 @@ def forward( for layer_idx, block in enumerate(self.blocks): if layer_idx in layers_to_collect: collected.append(x) - x, attn_weights = block(x, sequence_id, output_attentions=output_attentions) + x, attn_weights = block( + x, attention_mask, output_attentions=output_attentions, **kwargs + ) if output_attentions and attn_weights is not None: all_attentions.append(attn_weights) @@ -1010,15 +856,12 @@ class ESMCModel(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) - self._use_flash_attn = ( - _flash_attn_available and config._attn_implementation == "flash_attention_2" - ) self.embed = nn.Embedding(config.vocab_size, config.d_model) self.transformer = TransformerStack( + config, config.d_model, config.n_heads, config.n_layers, - use_flash_attn=self._use_flash_attn, ) self._sae_models: nn.ModuleDict = nn.ModuleDict() self.post_init() @@ -1197,53 +1040,48 @@ def forward( layers_to_collect = [] user_supplied_sequence_id = sequence_id is not None - if sequence_id is not None: + if user_supplied_sequence_id: bool_mask = sequence_id >= 0 else: if attention_mask is None: attention_mask = input_ids != self.config.pad_token_id assert attention_mask is not None bool_mask = attention_mask.bool() - sequence_id = bool_mask.to(torch.long) - 1 x = self.embed(input_ids) - b, l_ = x.shape[:2] - if self._use_flash_attn: - if user_supplied_sequence_id and (sequence_id > 0).any(): + if user_supplied_sequence_id: + if self.config._attn_implementation == "flash_attention_2" and ( + sequence_id > 0 + ).any(): raise ValueError( "Multi-chain ``sequence_id`` (any value > 0) is not " "supported with attn_implementation='flash_attention_2'. " "Re-load the model with attn_implementation='sdpa' (or " "'eager') for chain-aware attention masking." ) - assert unpad_input is not None - x, indices, *_ = unpad_input(x, bool_mask) + # Block-diagonal chain mask: a token attends only to tokens sharing + # its ``sequence_id``. Additive bias broadcast over heads, shape + # ``(batch, 1, seq_len, seq_len)``; handled by the eager / sdpa paths. + same_chain = sequence_id.unsqueeze(-1) == sequence_id.unsqueeze(-2) + attn_bias = torch.zeros( + same_chain.shape, dtype=x.dtype, device=x.device + ).masked_fill_(~same_chain, torch.finfo(x.dtype).min) + attn_bias = attn_bias.unsqueeze(1) else: - indices = None - - if self._use_flash_attn: - trans_seq_id = bool_mask - elif user_supplied_sequence_id: - trans_seq_id = sequence_id - elif bool_mask.all() and not output_attentions: - # Fused SDPA fast path (xformers / flash) is correct only when the - # mask is uniform; output_attentions forces the manual branch. - trans_seq_id = None - else: - trans_seq_id = sequence_id + attn_bias = create_bidirectional_mask( + config=self.config, + inputs_embeds=x, + attention_mask=attention_mask, + ) + last_hidden_state, _, collected, attentions = self.transformer( x, - sequence_id=trans_seq_id, + attention_mask=attn_bias, layers_to_collect=layers_to_collect, output_attentions=output_attentions, ) - if self._use_flash_attn: - assert indices is not None and pad_input is not None - last_hidden_state = pad_input(last_hidden_state, indices, b, l_) - collected = [pad_input(h, indices, b, l_) for h in collected] - # Stack once; reused for both SAE and hidden-state output. collected_tensor: torch.Tensor | None = ( torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] From a42a910f723142b8840bf4c3a6b7cef383c3c43f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 17:37:16 +0100 Subject: [PATCH 05/70] Remove transformer_engine dependency from ESMC (pure-PyTorch only) No other Transformers model depends on transformer_engine; drop it from ESMC. TE provided fused fp32-reduction LayerNorm+Linear / LayerNorm+MLP kernels, but the model already shipped pure-PyTorch fallbacks whose parameter names (`layer_norm_weight`, `fc1_weight`, `fc2_weight`, ...) match the published-checkpoint layout. Make those the only path. - Drop the `transformer_engine` import + `_te_available` guard + the "TE not installed" warning. - `_swiglu_ln_ffn` / `_make_attn_layernorm_qkv` / `_make_attn_out_proj` now unconditionally return the pure-PyTorch modules (`_PyTorchLayerNormMLP`, `_PyTorchLayerNormLinear`, `nn.Linear`). - Remove dead `_SwiGLU` class (the MLP fallback inlines silu(x1)*x2). If exact TE numerics (fp32-reduction LayerNorm) are ever required, that belongs in a Hub kernel via the `kernels` library, not a hard dependency. Verified: strict state_dict load from the pre-change baseline succeeds (parameter names unchanged -> published checkpoints still load), and last_hidden_state is bit-identical (0.0) at all valid positions for the plain, padding-mask, and multi-chain cases. Locally TE was never installed, so this is the exact path that already ran. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/modeling_esmc.py | 76 +++---------------- 1 file changed, 12 insertions(+), 64 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 2f7d1cf7cb8c..8ed4c44c1209 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -43,31 +43,6 @@ _CONFIG_FOR_DOC = "ESMCConfig" -# Transformer Engine: fused LayerNorm+Linear / LayerNorm+MLP kernels with -# fp32 reduction inside the LayerNorm. Recommended on GPU for accurate bf16 -# inference; without it the pure-PyTorch fallback drifts ~O(10) in fp32 and -# ~O(100) in bf16 on the unnormalized residual stream (perplexity stays -# within rounding noise). -try: - import transformer_engine.pytorch as te # type: ignore[import-untyped] - - _te_available = True -except ImportError: - te = None # type: ignore[assignment] - _te_available = False - -if not _te_available: - logger.warning( - "ESMC: transformer_engine is not installed; falling back to " - "pure-PyTorch LayerNorm+Linear / LayerNorm+MLP. Outputs will differ " - "numerically — measured on the unnormalized residual stream (before " - "the final LayerNorm), ~O(10) max-diff in fp32 and ~O(100) in bf16; " - "after the final LayerNorm these shrink to a few ULP and perplexity " - "stays within rounding noise. Install with " - "`pip install transformer-engine[pytorch]` to enable fused fp32-" - "reduction LayerNorm." - ) - # --------------------------------------------------------------------------- # Output dataclasses # --------------------------------------------------------------------------- @@ -381,18 +356,11 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: return int(((expansion_ratio * d_model) + 255) // 256 * 256) -class _SwiGLU(nn.Module): - """SwiGLU activation: ``silu(x1) * x2`` where ``x`` is split along the last dim.""" - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x1, x2 = x.chunk(2, dim=-1) - return F.silu(x1) * x2 - - class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection, sharing the parameter - names ``layer_norm_weight``, ``layer_norm_bias`` and ``weight`` so the - state-dict layout matches the accelerated TE module loaded on GPU. + """LayerNorm followed by a Linear projection. + + Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and + ``weight`` to match the published ESMC checkpoint layout. """ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: @@ -412,10 +380,11 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: class _PyTorchLayerNormMLP(nn.Module): - """LayerNorm + SwiGLU MLP, sharing the parameter names - ``layer_norm_weight``, ``layer_norm_bias``, ``fc1_weight``, - ``fc2_weight`` so the state-dict layout matches the accelerated TE - module loaded on GPU. + """LayerNorm + SwiGLU MLP. + + Parameters are named ``layer_norm_weight``, ``layer_norm_bias``, + ``fc1_weight`` and ``fc2_weight`` to match the published ESMC checkpoint + layout. """ def __init__( @@ -447,41 +416,20 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: - """LayerNorm + SwiGLU MLP. Uses Transformer Engine's fused LN+MLP when - available; otherwise returns the pure-PyTorch fallback with matching - state-dict layout.""" + """LayerNorm + SwiGLU MLP.""" assert not bias, "ESMC was trained with bias=False; bias=True not supported" hidden = _swiglu_hidden_dim(expansion_ratio, d_model) - if _te_available: - return te.LayerNormMLP( # type: ignore[union-attr] - hidden_size=d_model, - ffn_hidden_size=hidden, - bias=bias, - activation="swiglu", - init_method=None, - output_layer_init_method=None, - ) return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: - """LayerNorm + fused QKV projection. Uses Transformer Engine when - available; pure-PyTorch fallback otherwise.""" + """LayerNorm + fused QKV projection.""" assert not bias, "ESMC was trained with bias=False; bias=True not supported" - if _te_available: - return te.LayerNormLinear( # type: ignore[union-attr] - d_model, d_model * 3, bias=bias, init_method=None - ) return _PyTorchLayerNormLinear(d_model, d_model * 3) def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: - """Attention output projection. Uses Transformer Engine when available; - pure-PyTorch ``nn.Linear`` otherwise.""" - if _te_available: - return te.Linear( # type: ignore[union-attr] - d_model, d_model, bias=bias, init_method=None - ) + """Attention output projection.""" return nn.Linear(d_model, d_model, bias=bias) From 3c1c65b18cf289bbdbb85983c6adce18b880c007 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 17:49:34 +0100 Subject: [PATCH 06/70] Modernize ESMC rotary to the standard (cos, sin) + apply_rotary_pos_emb convention Replace the flash-attn-style cache-based rotary with the standard Transformers convention used by esm/llama, as the last code-shaping step before authoring modular_esmc.py. Before: a stateful `RotaryEmbedding` (per-attention-module) caching cos/sin, `forward(q, k)` returning rotated tensors, a custom `_apply` override to keep `inv_freq` fp32 across device casts, plus `_rotate_half` / `_apply_rotary_emb_torch`. After: - `rotate_half` + `apply_rotary_pos_emb` (identical to esm/llama). - `ESMCRotaryEmbedding(config)` -> `(cos, sin)`, computed once in `ESMCModel.forward` and threaded down (position_embeddings) through the stack/block to attention, mirroring esm. `inv_freq` is fp32 and non-persistent (matches the old behaviour: no rotary tensors in the checkpoint), and cos/sin are built in fp32 then cast. - Add `config.rope_theta` (default 10000.0, the previously-hardcoded base). - `_init_weights` recomputes `inv_freq` for `ESMCRotaryEmbedding` (meta-init safe). Verified: strict state_dict load from the saved baseline succeeds (rotary buffers are non-persistent, so keys are unchanged -> published checkpoints still load), and last_hidden_state is bit-identical (0.0) at all valid positions for plain, padding-mask, and multi-chain. The fp32 matmul-based freqs equal the old `outer(t, inv_freq)`; same RoPE math, idiomatic shape. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmc/configuration_esmc.py | 4 + src/transformers/models/esmc/modeling_esmc.py | 266 ++++++------------ 2 files changed, 89 insertions(+), 181 deletions(-) diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py index 962a23e46163..8d97ff9860f1 100644 --- a/src/transformers/models/esmc/configuration_esmc.py +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -42,6 +42,8 @@ class ESMCConfig(PretrainedConfig): The standard deviation of the truncated normal initialiser for weight matrix initialisation. classifier_dropout (`float`, *optional*, defaults to 0.1): Dropout ratio for the classification head. + rope_theta (`float`, *optional*, defaults to 10000.0): + The base period of the rotary position embeddings (RoPE). Examples: @@ -71,6 +73,7 @@ def __init__( mask_token_id: int = 32, initializer_range: float = 0.02, classifier_dropout: float = 0.1, + rope_theta: float = 10000.0, **kwargs, ): super().__init__( @@ -83,6 +86,7 @@ def __init__( self.n_layers = n_layers self.initializer_range = initializer_range self.classifier_dropout = classifier_dropout + self.rope_theta = rope_theta self.tie_word_embeddings = False diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 8ed4c44c1209..6971d3ee39aa 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -164,186 +164,83 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): # --------------------------------------------------------------------------- -def _rotate_half(x: torch.Tensor, interleaved: bool = False) -> torch.Tensor: - if not interleaved: - x1, x2 = x.chunk(2, dim=-1) - return torch.cat((-x2, x1), dim=-1) - x1, x2 = x[..., ::2], x[..., 1::2] - return torch.stack((-x2, x1), dim=-1).flatten(-2, -1) +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) -def _apply_rotary_emb_torch( - x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, interleaved: bool = False -) -> torch.Tensor: - """Apply rotary position embeddings (pure PyTorch, no Triton dependency). +def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): + """Apply Rotary Position Embedding to the query and key tensors. - Args: - x: ``(batch, seqlen, n_heads, head_dim)`` - cos: ``(seqlen, rotary_dim / 2)`` - sin: ``(seqlen, rotary_dim / 2)`` + ``q`` / ``k`` are ``(batch, n_heads, seq_len, head_dim)`` and ``cos`` / + ``sin`` are ``(batch, seq_len, head_dim)``; ``unsqueeze_dim=1`` broadcasts + them over the head axis. Computed in fp32, then cast back to the input dtype. """ - ro_dim = cos.shape[-1] * 2 - seqlen = x.size(1) - cos = cos[:seqlen].unsqueeze(1).repeat(1, 1, 2) - sin = sin[:seqlen].unsqueeze(1).repeat(1, 1, 2) - return torch.cat( - [ - x[..., :ro_dim] * cos + _rotate_half(x[..., :ro_dim], interleaved) * sin, - x[..., ro_dim:], - ], - dim=-1, - ) - + original_dtype = q.dtype + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_embed = (q.float() * cos) + (rotate_half(q.float()) * sin) + k_embed = (k.float() * cos) + (rotate_half(k.float()) * sin) + return q_embed.to(original_dtype), k_embed.to(original_dtype) -class RotaryEmbedding(nn.Module): - """Rotary position embeddings (RoPE) as described in `RoFormer`_. - .. _RoFormer: https://arxiv.org/abs/2104.09864 +class ESMCRotaryEmbedding(nn.Module): + """Rotary position embeddings (RoPE), config-driven, returning ``(cos, sin)``. - Args: - dim: Size of a single attention head. - base: Frequency base for the sinusoidal positions. - interleaved: If ``True`` rotate adjacent pairs (GPT-J style) instead of - splitting the head dimension in half (GPT-NeoX style). - scaling_factor: Linear scaling factor applied to position indices. - pos_idx_in_fp32: Compute position indices in float32 to avoid bf16 - rounding errors at large sequence lengths. + Follows the standard Transformers rotary convention (cf. + ``EsmRotaryEmbedding`` / ``LlamaRotaryEmbedding``): ``inv_freq`` is a + non-persistent fp32 buffer and ``forward`` builds full-head-dim ``cos`` / + ``sin`` in fp32 before casting to the input dtype. """ - def __init__( - self, - dim: int, - base: float = 10000.0, - interleaved: bool = False, - scale_base: float | None = None, - scaling_factor: float = 1.0, - pos_idx_in_fp32: bool = True, - device=None, - ): + inv_freq: torch.Tensor + + def __init__(self, config: ESMCConfig, device=None): super().__init__() - self.dim = dim - self.base = base - self.interleaved = interleaved - self.scale_base = scale_base - self.scaling_factor = scaling_factor - self.pos_idx_in_fp32 = pos_idx_in_fp32 - - self._seq_len_cached = 0 - self._cos_cached: torch.Tensor | None = None - self._sin_cached: torch.Tensor | None = None - self._cos_k_cached: torch.Tensor | None = None - self._sin_k_cached: torch.Tensor | None = None - - self.reset_parameters(device=device) - - def reset_parameters(self, device=None): - inv_freq = self._compute_inv_freq(device) - self.register_buffer("inv_freq", inv_freq, persistent=False) - arange = torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) - scale = ( - (arange + 0.4 * self.dim) / (1.4 * self.dim) - if self.scale_base is not None - else None + self.config = config + self.register_buffer( + "inv_freq", self._compute_inv_freq(config, device), persistent=False ) - self.register_buffer("scale", scale, persistent=False) - def _compute_inv_freq(self, device=None) -> torch.Tensor: + @staticmethod + def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: + dim = config.d_model // config.n_heads return 1.0 / ( - self.base + config.rope_theta ** ( - torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) - / self.dim + torch.arange(0, dim, 2, dtype=torch.int64).to( + device=device, dtype=torch.float32 + ) + / dim ) ) - def _update_cos_sin_cache(self, seqlen: int, device=None, dtype=None): - if self.inv_freq.is_meta: - self.reset_parameters(device=device) - if ( - seqlen > self._seq_len_cached - or self._cos_cached is None - or self._cos_cached.device != device - or self._cos_cached.dtype != dtype - or (self.training and self._cos_cached.is_inference()) - ): - self._seq_len_cached = seqlen - if self.pos_idx_in_fp32: - t = ( - torch.arange(seqlen, device=device, dtype=torch.float32) - / self.scaling_factor - ) - inv_freq = ( - self.inv_freq.to(torch.float32) - if self.inv_freq.dtype != torch.float32 - else self.inv_freq - ) - else: - t = ( - torch.arange(seqlen, device=device, dtype=self.inv_freq.dtype) # type: ignore[call-overload] - / self.scaling_factor - ) - inv_freq = self.inv_freq - freqs = torch.outer(t, inv_freq) # type: ignore[arg-type] - - if self.scale is None: - self._cos_cached = torch.cos(freqs).to(dtype) - self._sin_cached = torch.sin(freqs).to(dtype) - else: - _scale: torch.Tensor = self.scale # type: ignore[assignment] - power = ( - torch.arange(seqlen, dtype=_scale.dtype, device=_scale.device) - - seqlen // 2 - ) / self.scale_base # type: ignore[operator] - scale = _scale.to(device=power.device) ** power.unsqueeze(-1) - self._cos_cached = (torch.cos(freqs) * scale).to(dtype) - self._sin_cached = (torch.sin(freqs) * scale).to(dtype) - self._cos_k_cached = (torch.cos(freqs) / scale).to(dtype) - self._sin_k_cached = (torch.sin(freqs) / scale).to(dtype) - - def _apply(self, fn, recurse=True): - if self.inv_freq.is_meta: - self.reset_parameters(device="cpu") - result = super()._apply(fn, recurse=recurse) - # Recompute inv_freq on the new device: CPU vs CUDA ``pow`` differ by - # ~1 fp32 ULP, which compounds across attention layers. Keep this - # buffer fp32 even when the module is cast to bf16/fp16; otherwise the - # rounded RoPE frequencies drift from the internal ESMC path. - new_inv_freq = self._compute_inv_freq(device=self.inv_freq.device) - self.register_buffer("inv_freq", new_inv_freq, persistent=False) - self._seq_len_cached = 0 - self._cos_cached = None - self._sin_cached = None - self._cos_k_cached = None - self._sin_k_cached = None - return result - + @torch.no_grad() def forward( - self, q: torch.Tensor, k: torch.Tensor, seqlen_offset: int = 0 + self, x: torch.Tensor, position_ids: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor]: - """Apply RoPE to query and key tensors. - - Args: - q: ``(batch, seqlen, n_heads, head_dim)`` - k: ``(batch, seqlen, n_heads, head_dim)`` - seqlen_offset: Offset used in incremental decoding. - - Returns: - Tuple of rotated ``(q, k)`` tensors with the same shape as the inputs. - """ - self._update_cos_sin_cache( - q.shape[1] + seqlen_offset, device=q.device, dtype=q.dtype + inv_freq_expanded = ( + self.inv_freq[None, :, None] + .float() + .expand(position_ids.shape[0], -1, 1) + .to(x.device) ) - assert self._cos_cached is not None and self._sin_cached is not None - - if self.scale is not None: - raise NotImplementedError("XPos scaling is not supported in this path.") - - cos = self._cos_cached[seqlen_offset:] - sin = self._sin_cached[seqlen_offset:] - - q_rot = _apply_rotary_emb_torch(q, cos, sin, self.interleaved) - k_rot = _apply_rotary_emb_torch(k, cos, sin, self.interleaved) - return q_rot, k_rot + position_ids_expanded = position_ids[:, None, :].float() + device_type = ( + x.device.type + if isinstance(x.device.type, str) and x.device.type != "mps" + else "cpu" + ) + with torch.autocast(device_type=device_type, enabled=False): # force fp32 + freqs = ( + inv_freq_expanded.float() @ position_ids_expanded.float() + ).transpose(1, 2) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() + sin = emb.sin() + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) # --------------------------------------------------------------------------- @@ -515,22 +412,11 @@ def __init__( self.q_ln = nn.Identity() self.k_ln = nn.Identity() - self.rotary = RotaryEmbedding(d_model // n_heads) - - def _apply_rotary( - self, q: torch.Tensor, k: torch.Tensor - ) -> tuple[torch.Tensor, torch.Tensor]: - q = q.unflatten(-1, (self.n_heads, self.d_head)) - k = k.unflatten(-1, (self.n_heads, self.d_head)) - q, k = self.rotary(q, k) - q = q.flatten(-2, -1) - k = k.flatten(-2, -1) - return q, k - def forward( self, hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, + position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, output_attentions: bool = False, **kwargs, ) -> tuple[torch.Tensor, torch.Tensor | None]: @@ -538,23 +424,27 @@ def forward( Attention is computed by the backend selected through ``config._attn_implementation`` (``eager`` / ``sdpa`` / - ``flash_attention_2`` / ...). RoPE and QK-LayerNorm are applied here; the - backend only computes ``softmax(QKᵀ)V``. ``attn_weights`` is ``None`` - unless the backend exposes the probabilities — ``output_attentions=True`` - forces the ``eager`` interface so they are observable. + ``flash_attention_2`` / ...). QK-LayerNorm and RoPE (via + ``position_embeddings``) are applied here; the backend only computes + ``softmax(QKᵀ)V``. ``attn_weights`` is ``None`` unless the backend + exposes the probabilities — ``output_attentions=True`` forces the + ``eager`` interface so they are observable. """ b, s, _ = hidden_states.shape qkv = self.layernorm_qkv(hidden_states) q, k, v = torch.chunk(qkv, 3, dim=-1) q = self.q_ln(q).to(q.dtype) k = self.k_ln(k).to(q.dtype) - q, k = self._apply_rotary(q, k) # (B, S, D) -> (B, H, S, Dh) q = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) k = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) v = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + if position_embeddings is not None: + cos, sin = position_embeddings + q, k = apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1) + attention_interface: Callable = eager_attention_forward if not output_attentions: attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( @@ -627,6 +517,7 @@ def forward( self, x: torch.Tensor, attention_mask: torch.Tensor | None = None, + position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, output_attentions: bool = False, **kwargs, ) -> tuple[torch.Tensor, torch.Tensor | None]: @@ -645,7 +536,11 @@ def forward( ``(batch, num_heads, seq_len, seq_len)`` or ``None``. """ attn_out, attn_weights = self.attn( - x, attention_mask, output_attentions=output_attentions, **kwargs + x, + attention_mask, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, ) x = x + attn_out / self.scaling_factor x = x + self.ffn(x) / self.scaling_factor @@ -704,6 +599,7 @@ def forward( self, x: torch.Tensor, attention_mask: torch.Tensor | None = None, + position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, layers_to_collect: list[int] | None = None, output_attentions: bool = False, **kwargs, @@ -740,7 +636,11 @@ def forward( if layer_idx in layers_to_collect: collected.append(x) x, attn_weights = block( - x, attention_mask, output_attentions=output_attentions, **kwargs + x, + attention_mask, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, ) if output_attentions and attn_weights is not None: all_attentions.append(attn_weights) @@ -780,8 +680,8 @@ def _init_weights(self, module: nn.Module): module.weight.data.normal_(mean=0.0, std=std) if module.bias is not None: module.bias.data.zero_() - elif isinstance(module, RotaryEmbedding): - module.reset_parameters(device=self.device) + elif isinstance(module, ESMCRotaryEmbedding): + module.inv_freq = module._compute_inv_freq(module.config, device=self.device) # --------------------------------------------------------------------------- @@ -805,6 +705,7 @@ class ESMCModel(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) + self.rotary_emb = ESMCRotaryEmbedding(config) self.transformer = TransformerStack( config, config.d_model, @@ -997,6 +898,8 @@ def forward( bool_mask = attention_mask.bool() x = self.embed(input_ids) + position_ids = torch.arange(x.shape[1], device=x.device).unsqueeze(0) + position_embeddings = self.rotary_emb(x, position_ids) if user_supplied_sequence_id: if self.config._attn_implementation == "flash_attention_2" and ( @@ -1026,6 +929,7 @@ def forward( last_hidden_state, _, collected, attentions = self.transformer( x, attention_mask=attn_bias, + position_embeddings=position_embeddings, layers_to_collect=layers_to_collect, output_attentions=output_attentions, ) From abfe0a05b3abe140996bcefdc560695e80d954bd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 13:56:28 +0100 Subject: [PATCH 07/70] Add modular_esmc.py; generate modeling_esmc.py from it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESMC now follows the modular convention. modular_esmc.py is the source of truth; modeling_esmc.py is generated by utils/modular_model_converter.py and carries the auto-generated header. Reuse from esm (the natural parent — also a bidirectional protein encoder): `eager_attention_forward`, `rotate_half`, and `apply_rotary_pos_emb` are now imported from ..esm.modeling_esm and inlined into the generated file with `# Copied from` headers (so they stay in sync). `rotate_half` is pulled in transitively as a dependency of `apply_rotary_pos_emb`, matching the qwen3 pattern. Everything else stays ESMC-specific and is defined in the modular file: the SAE-integrated ESMCModel + ForMaskedLM/SequenceClassification/ TokenClassification, the fused-LN MultiHeadAttention, SwiGLU FFN, TransformerStack, ESMCRotaryEmbedding, and the SAE-carrying output dataclasses. As expected for this architecture the dedup is modest; the win is convention compliance + auto-sync of the shared functions. The modular file was ruff-fixed/formatted (Optional[X] -> X | None, import order) before regeneration, so both files are now ruff-clean. Verified: `check_modular_conversion.py` passes (files in sync); `transformers` imports; and loading identical weights reproduces the pre-conversion last_hidden_state bit-for-bit (0.0) at all valid positions for plain, padding-mask, and multi-chain inputs. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/modeling_esmc.py | 340 ++--- src/transformers/models/esmc/modular_esmc.py | 1245 +++++++++++++++++ 2 files changed, 1391 insertions(+), 194 deletions(-) create mode 100644 src/transformers/models/esmc/modular_esmc.py diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 6971d3ee39aa..010619aed833 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -1,3 +1,9 @@ +# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 +# This file was automatically generated from src/transformers/models/esmc/modular_esmc.py. +# Do NOT edit this file manually as any edits will be overwritten by the generation of +# the file from the modular. If any change should be done, please apply the change to the +# modular_esmc.py file directly. One of our CI enforces this. +# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 # Copyright 2026 Biohub. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,18 +17,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""PyTorch ESMC model.""" import math import re +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable, Optional, cast +from typing import cast import torch import torch.nn as nn from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss from torch.nn import functional as F +from ...integrations import use_kernel_func_from_hub from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] MaskedLMOutput, @@ -31,17 +38,11 @@ TokenClassifierOutput, ) from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel # type: ignore[import] -from ...utils import ( # type: ignore[import] - auto_docstring, - can_return_tuple, - logging, -) +from ...processing_utils import Unpack +from ...utils import TransformersKwargs, auto_docstring, can_return_tuple # type: ignore[import] from .configuration_esmc import ESMCConfig from .modeling_esmc_sae import _ESMCSAELayer -logger = logging.get_logger(__name__) - -_CONFIG_FOR_DOC = "ESMCConfig" # --------------------------------------------------------------------------- # Output dataclasses @@ -164,28 +165,6 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): # --------------------------------------------------------------------------- -def rotate_half(x): - """Rotates half the hidden dims of the input.""" - x1 = x[..., : x.shape[-1] // 2] - x2 = x[..., x.shape[-1] // 2 :] - return torch.cat((-x2, x1), dim=-1) - - -def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): - """Apply Rotary Position Embedding to the query and key tensors. - - ``q`` / ``k`` are ``(batch, n_heads, seq_len, head_dim)`` and ``cos`` / - ``sin`` are ``(batch, seq_len, head_dim)``; ``unsqueeze_dim=1`` broadcasts - them over the head axis. Computed in fp32, then cast back to the input dtype. - """ - original_dtype = q.dtype - cos = cos.unsqueeze(unsqueeze_dim) - sin = sin.unsqueeze(unsqueeze_dim) - q_embed = (q.float() * cos) + (rotate_half(q.float()) * sin) - k_embed = (k.float() * cos) + (rotate_half(k.float()) * sin) - return q_embed.to(original_dtype), k_embed.to(original_dtype) - - class ESMCRotaryEmbedding(nn.Module): """Rotary position embeddings (RoPE), config-driven, returning ``(cos, sin)``. @@ -200,59 +179,29 @@ class ESMCRotaryEmbedding(nn.Module): def __init__(self, config: ESMCConfig, device=None): super().__init__() self.config = config - self.register_buffer( - "inv_freq", self._compute_inv_freq(config, device), persistent=False - ) + self.register_buffer("inv_freq", self._compute_inv_freq(config, device), persistent=False) @staticmethod def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: dim = config.d_model // config.n_heads return 1.0 / ( config.rope_theta - ** ( - torch.arange(0, dim, 2, dtype=torch.int64).to( - device=device, dtype=torch.float32 - ) - / dim - ) + ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float32) / dim) ) @torch.no_grad() - def forward( - self, x: torch.Tensor, position_ids: torch.Tensor - ) -> tuple[torch.Tensor, torch.Tensor]: - inv_freq_expanded = ( - self.inv_freq[None, :, None] - .float() - .expand(position_ids.shape[0], -1, 1) - .to(x.device) - ) + def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) position_ids_expanded = position_ids[:, None, :].float() - device_type = ( - x.device.type - if isinstance(x.device.type, str) and x.device.type != "mps" - else "cpu" - ) + device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" with torch.autocast(device_type=device_type, enabled=False): # force fp32 - freqs = ( - inv_freq_expanded.float() @ position_ids_expanded.float() - ).transpose(1, 2) + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) emb = torch.cat((freqs, freqs), dim=-1) cos = emb.cos() sin = emb.sin() return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) -# --------------------------------------------------------------------------- -# Feed-forward network helpers -# --------------------------------------------------------------------------- - - -def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: - """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" - return int(((expansion_ratio * d_model) + 255) // 256 * 256) - - class _PyTorchLayerNormLinear(nn.Module): """LayerNorm followed by a Linear projection. @@ -270,9 +219,7 @@ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: nn.init.normal_(self.weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm( - x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps - ) + x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) return F.linear(x, self.weight) @@ -284,9 +231,7 @@ class _PyTorchLayerNormMLP(nn.Module): layout. """ - def __init__( - self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5 - ) -> None: + def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> None: super().__init__() self.hidden_size = hidden_size self.ffn_hidden_size = ffn_hidden_size @@ -312,37 +257,38 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return F.linear(x, self.fc2_weight) -def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: - """LayerNorm + SwiGLU MLP.""" - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - hidden = _swiglu_hidden_dim(expansion_ratio, d_model) - return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) - - -def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: - """LayerNorm + fused QKV projection.""" - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - return _PyTorchLayerNormLinear(d_model, d_model * 3) - - -def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: - """Attention output projection.""" - return nn.Linear(d_model, d_model, bias=bias) - +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) -def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequential: - hidden = int(expansion_ratio * d_model) - return nn.Sequential( - nn.LayerNorm(d_model), - nn.Linear(d_model, hidden, bias=bias), - nn.GELU(), - nn.Linear(hidden, d_model, bias=bias), - ) +@use_kernel_func_from_hub("rotary_pos_emb") +def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): + """Applies Rotary Position Embedding to the query and key tensors. -# --------------------------------------------------------------------------- -# Attention -# --------------------------------------------------------------------------- + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + original_dtype = q.dtype + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_embed = (q.float() * cos) + (rotate_half(q.float()) * sin) + k_embed = (k.float() * cos) + (rotate_half(k.float()) * sin) + return q_embed.to(original_dtype), k_embed.to(original_dtype) def eager_attention_forward( @@ -353,27 +299,42 @@ def eager_attention_forward( attention_mask: torch.Tensor | None, scaling: float | None = None, dropout: float = 0.0, - **kwargs, -) -> tuple[torch.Tensor, torch.Tensor]: - """Reference attention ``softmax(Q @ Kᵀ * scaling + mask) @ V``. - - Operates on ``(batch, n_heads, seq_len, head_dim)`` query/key/value and - returns ``(attn_output, attn_weights)``; ``attn_output`` is transposed back - to ``(batch, seq_len, n_heads, head_dim)`` to match the other - ``ALL_ATTENTION_FUNCTIONS`` backends. - """ + **kwargs: Unpack[TransformersKwargs], +): if scaling is None: scaling = query.size(-1) ** -0.5 + + # Take the dot product between "query" and "key" to get the raw attention scores. attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling + if attention_mask is not None: - attn_weights = attn_weights + attention_mask[:, :, :, : key.shape[-2]] + attn_weights = attn_weights + attention_mask + attn_weights = nn.functional.softmax(attn_weights, dim=-1) attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) + attn_output = torch.matmul(attn_weights, value) attn_output = attn_output.transpose(1, 2).contiguous() + return attn_output, attn_weights +def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: + """LayerNorm + fused QKV projection.""" + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + return _PyTorchLayerNormLinear(d_model, d_model * 3) + + +def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: + """Attention output projection.""" + return nn.Linear(d_model, d_model, bias=bias) + + +# --------------------------------------------------------------------------- +# Attention +# --------------------------------------------------------------------------- + + class MultiHeadAttention(nn.Module): """Multi-head self-attention with QK LayerNorm and RoPE. @@ -465,6 +426,33 @@ def forward( return self.out_proj(attn_output), attn_weights +# --------------------------------------------------------------------------- +# Feed-forward network helpers +# --------------------------------------------------------------------------- + + +def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: + """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" + return int(((expansion_ratio * d_model) + 255) // 256 * 256) + + +def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: + """LayerNorm + SwiGLU MLP.""" + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + hidden = _swiglu_hidden_dim(expansion_ratio, d_model) + return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) + + +def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequential: + hidden = int(expansion_ratio * d_model) + return nn.Sequential( + nn.LayerNorm(d_model), + nn.Linear(d_model, hidden, bias=bias), + nn.GELU(), + nn.Linear(hidden, d_model, bias=bias), + ) + + # --------------------------------------------------------------------------- # Transformer blocks # --------------------------------------------------------------------------- @@ -498,18 +486,14 @@ def __init__( ): super().__init__() - self.attn = MultiHeadAttention( - config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm - ) + self.attn = MultiHeadAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) if ffn_type == "swiglu": self.ffn = _swiglu_ln_ffn(d_model, expansion_ratio, bias) elif ffn_type == "gelu": self.ffn = _gelu_ln_ffn(d_model, expansion_ratio, bias) else: - raise ValueError( - f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'." - ) + raise ValueError(f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'.") self.scaling_factor = residue_scaling_factor @@ -582,9 +566,7 @@ def __init__( config, d_model, n_heads, - residue_scaling_factor=math.sqrt(n_layers / 36) - if scale_residue - else 1.0, + residue_scaling_factor=math.sqrt(n_layers / 36) if scale_residue else 1.0, expansion_ratio=expansion_ratio, bias=bias, qk_layernorm=qk_layernorm, @@ -739,8 +721,7 @@ def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: """ for layer in sae_models: assert isinstance(layer, _ESMCSAELayer), ( - f"Expected an SAE layer (model.layers['']), got " - f"{type(layer).__name__}." + f"Expected an SAE layer (model.layers['']), got {type(layer).__name__}." ) key = f"layer{int(layer.layer)}" if key in self._sae_models: @@ -757,15 +738,12 @@ def _get_sae_layer_num_requested(self, model_name: str) -> int: """Recover the backbone-layer index from a key written by :meth:`add_sae_models` (``"layer{N}"`` → ``N``).""" match = self._SAE_KEY_RE.fullmatch(model_name) - assert ( - match is not None - ), f"Unexpected SAE key {model_name!r}; expected 'layer{{N}}'." + assert match is not None, f"Unexpected SAE key {model_name!r}; expected 'layer{{N}}'." return int(match.group(1)) def _validate_sae_inputs(self, input_ids: torch.Tensor) -> None: assert torch.all(input_ids != self.config.mask_token_id), ( - "SAE inputs must not contain mask tokens. " - "SAEs were trained on unmasked sequences." + "SAE inputs must not contain mask tokens. SAEs were trained on unmasked sequences." ) def _get_sae_outputs( @@ -817,12 +795,12 @@ def _get_sae_outputs( @auto_docstring def forward( self, - input_ids: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - sequence_id: Optional[torch.Tensor] = None, - output_hidden_states: Optional[bool] = None, - output_attentions: Optional[bool] = None, - return_dict: Optional[bool] = None, + input_ids: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + sequence_id: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, compute_sae: bool = True, normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCOutput: @@ -861,18 +839,10 @@ def forward( ``` """ output_hidden_states = ( - output_hidden_states - if output_hidden_states is not None - else self.config.output_hidden_states - ) - output_attentions = ( - output_attentions - if output_attentions is not None - else self.config.output_attentions - ) - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states ) + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + return_dict = return_dict if return_dict is not None else self.config.use_return_dict output_sae = compute_sae and len(self._sae_models) > 0 @@ -882,9 +852,7 @@ def forward( if output_hidden_states: layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) elif output_sae: - layers_to_collect = sorted( - {self._get_sae_layer_num_requested(name) for name in self._sae_models} - ) + layers_to_collect = sorted({self._get_sae_layer_num_requested(name) for name in self._sae_models}) else: layers_to_collect = [] @@ -902,9 +870,7 @@ def forward( position_embeddings = self.rotary_emb(x, position_ids) if user_supplied_sequence_id: - if self.config._attn_implementation == "flash_attention_2" and ( - sequence_id > 0 - ).any(): + if self.config._attn_implementation == "flash_attention_2" and (sequence_id > 0).any(): raise ValueError( "Multi-chain ``sequence_id`` (any value > 0) is not " "supported with attn_implementation='flash_attention_2'. " @@ -915,9 +881,9 @@ def forward( # its ``sequence_id``. Additive bias broadcast over heads, shape # ``(batch, 1, seq_len, seq_len)``; handled by the eager / sdpa paths. same_chain = sequence_id.unsqueeze(-1) == sequence_id.unsqueeze(-2) - attn_bias = torch.zeros( - same_chain.shape, dtype=x.dtype, device=x.device - ).masked_fill_(~same_chain, torch.finfo(x.dtype).min) + attn_bias = torch.zeros(same_chain.shape, dtype=x.dtype, device=x.device).masked_fill_( + ~same_chain, torch.finfo(x.dtype).min + ) attn_bias = attn_bias.unsqueeze(1) else: attn_bias = create_bidirectional_mask( @@ -943,9 +909,7 @@ def forward( if output_sae and collected_tensor is not None: assert input_ids is not None self._validate_sae_inputs(input_ids) - sae_outputs = self._get_sae_outputs( - collected_tensor, layers_to_collect, bool_mask, normalize_sae - ) + sae_outputs = self._get_sae_outputs(collected_tensor, layers_to_collect, bool_mask, normalize_sae) hidden_states_tensor = collected_tensor if output_hidden_states else None @@ -974,9 +938,7 @@ def forward( # --------------------------------------------------------------------------- -def _esmc_lm_head( - d_model: int, output_dim: int, hidden_dim: int | None = None -) -> nn.Sequential: +def _esmc_lm_head(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Sequential: """Linear → GELU → LayerNorm → Linear projection head for masked LM.""" hidden_dim = hidden_dim if hidden_dim is not None else d_model return nn.Sequential( @@ -1021,13 +983,13 @@ def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: @auto_docstring def forward( self, - input_ids: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - sequence_id: Optional[torch.Tensor] = None, - output_hidden_states: Optional[bool] = None, - output_attentions: Optional[bool] = None, - return_dict: Optional[bool] = None, - labels: Optional[torch.Tensor] = None, + input_ids: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + sequence_id: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + labels: torch.Tensor | None = None, compute_sae: bool = True, normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: @@ -1060,9 +1022,7 @@ def forward( torch.Size([1, 11, 64]) ``` """ - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict encoder_outputs = self.esmc( input_ids=input_ids, @@ -1079,9 +1039,7 @@ def forward( loss: torch.Tensor | None = None if labels is not None: - loss = CrossEntropyLoss(ignore_index=-100)( - logits.view(-1, self.config.vocab_size), labels.view(-1) - ) + loss = CrossEntropyLoss(ignore_index=-100)(logits.view(-1, self.config.vocab_size), labels.view(-1)) if not return_dict: return tuple( @@ -1158,12 +1116,12 @@ def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: @auto_docstring def forward( self, - input_ids: Optional[torch.LongTensor] = None, - attention_mask: Optional[torch.Tensor] = None, - output_hidden_states: Optional[bool] = None, - output_attentions: Optional[bool] = None, - return_dict: Optional[bool] = None, - labels: Optional[torch.Tensor] = None, + input_ids: torch.LongTensor | None = None, + attention_mask: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + labels: torch.Tensor | None = None, compute_sae: bool = True, normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: @@ -1180,9 +1138,7 @@ def forward( normalize_sae (`bool`, *optional*, defaults to ``False``): When ``True``, scale SAE features by ``idf / max`` normalization buffers. """ - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict encoder_outputs = self.esmc( input_ids, @@ -1214,9 +1170,7 @@ def forward( labels.squeeze() if self.num_labels == 1 else labels, ) elif self.config.problem_type == "single_label_classification": - loss = CrossEntropyLoss()( - logits.view(-1, self.num_labels), labels.view(-1) - ) + loss = CrossEntropyLoss()(logits.view(-1, self.num_labels), labels.view(-1)) elif self.config.problem_type == "multi_label_classification": loss = BCEWithLogitsLoss()(logits, labels) @@ -1273,12 +1227,12 @@ def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: @auto_docstring def forward( self, - input_ids: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - output_hidden_states: Optional[bool] = None, - output_attentions: Optional[bool] = None, - return_dict: Optional[bool] = None, - labels: Optional[torch.Tensor] = None, + input_ids: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + labels: torch.Tensor | None = None, compute_sae: bool = True, normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: @@ -1294,9 +1248,7 @@ def forward( normalize_sae (`bool`, *optional*, defaults to ``False``): When ``True``, scale SAE features by ``idf / max`` normalization buffers. """ - return_dict = ( - return_dict if return_dict is not None else self.config.use_return_dict - ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict encoder_outputs = self.esmc( input_ids=input_ids, diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py new file mode 100644 index 000000000000..e439a5b2c64b --- /dev/null +++ b/src/transformers/models/esmc/modular_esmc.py @@ -0,0 +1,1245 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""PyTorch ESMC model.""" + +import math +import re +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +import torch +import torch.nn as nn +from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss +from torch.nn import functional as F + +from ...masking_utils import create_bidirectional_mask # type: ignore[import] +from ...modeling_outputs import ( # type: ignore[import] + MaskedLMOutput, + ModelOutput, + SequenceClassifierOutput, + TokenClassifierOutput, +) +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel # type: ignore[import] +from ...utils import ( # type: ignore[import] + auto_docstring, + can_return_tuple, + logging, +) +from ..esm.modeling_esm import ( + apply_rotary_pos_emb, + eager_attention_forward, +) +from .configuration_esmc import ESMCConfig +from .modeling_esmc_sae import _ESMCSAELayer + + +logger = logging.get_logger(__name__) + +_CONFIG_FOR_DOC = "ESMCConfig" + +# --------------------------------------------------------------------------- +# Output dataclasses +# --------------------------------------------------------------------------- + + +@dataclass +class ESMCOutput(ModelOutput): + """ + Args: + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Sequence of hidden states at the output of the last layer, after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states for all encoder layers. + Shape ``(n_layers, batch_size, sequence_length, d_model)``. + Returned when ``output_hidden_states=True``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + Only populated when SAE models have been registered via + ``add_sae_models`` and ``compute_sae=True``. + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. Not available on the + ``flash_attention_2`` path. + """ + + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +@dataclass +class ESMCMaskedLMOutput(MaskedLMOutput): + """ + Args: + loss (`torch.FloatTensor` of shape `(1,)`, *optional*): + Masked language modelling loss. Returned when ``labels`` are provided. + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, vocab_size)`): + Prediction scores of the language modelling head. + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Final hidden states after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. + """ + + loss: torch.FloatTensor | None = None + logits: torch.FloatTensor | None = None + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +@dataclass +class ESMCTokenClassifierOutput(TokenClassifierOutput): + """ + Args: + loss (`torch.FloatTensor` of shape `(1,)`, *optional*): + Token classification loss. Returned when ``labels`` are provided. + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, num_labels)`): + Classification scores (before SoftMax). + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Final hidden states after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. + """ + + loss: torch.FloatTensor | None = None + logits: torch.FloatTensor | None = None + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +@dataclass +class ESMCSequenceClassifierOutput(SequenceClassifierOutput): + """ + Args: + loss (`torch.FloatTensor` of shape `(1,)`, *optional*): + Sequence classification loss. Returned when ``labels`` are provided. + logits (`torch.FloatTensor` of shape `(batch_size, num_labels)`): + Classification scores (before SoftMax). + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): + Final hidden states after layer normalisation. + hidden_states (`torch.FloatTensor`, *optional*): + Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + sae_outputs (`dict[str, torch.Tensor]`, *optional*): + SAE feature magnitudes keyed by SAE model name (sparse tensors). + attentions (`tuple(torch.FloatTensor)`, *optional*): + Per-layer attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Returned when ``output_attentions=True``. + """ + + loss: torch.FloatTensor | None = None + logits: torch.FloatTensor | None = None + last_hidden_state: torch.FloatTensor | None = None + hidden_states: torch.FloatTensor | None = None + sae_outputs: dict[str, torch.Tensor] | None = None + attentions: tuple[torch.FloatTensor, ...] | None = None + + +# --------------------------------------------------------------------------- +# Rotary position embedding helpers +# --------------------------------------------------------------------------- + + +class ESMCRotaryEmbedding(nn.Module): + """Rotary position embeddings (RoPE), config-driven, returning ``(cos, sin)``. + + Follows the standard Transformers rotary convention (cf. + ``EsmRotaryEmbedding`` / ``LlamaRotaryEmbedding``): ``inv_freq`` is a + non-persistent fp32 buffer and ``forward`` builds full-head-dim ``cos`` / + ``sin`` in fp32 before casting to the input dtype. + """ + + inv_freq: torch.Tensor + + def __init__(self, config: ESMCConfig, device=None): + super().__init__() + self.config = config + self.register_buffer("inv_freq", self._compute_inv_freq(config, device), persistent=False) + + @staticmethod + def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: + dim = config.d_model // config.n_heads + return 1.0 / ( + config.rope_theta + ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float32) / dim) + ) + + @torch.no_grad() + def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) + position_ids_expanded = position_ids[:, None, :].float() + device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" + with torch.autocast(device_type=device_type, enabled=False): # force fp32 + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() + sin = emb.sin() + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) + + +# --------------------------------------------------------------------------- +# Feed-forward network helpers +# --------------------------------------------------------------------------- + + +def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: + """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" + return int(((expansion_ratio * d_model) + 255) // 256 * 256) + + +class _PyTorchLayerNormLinear(nn.Module): + """LayerNorm followed by a Linear projection. + + Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and + ``weight`` to match the published ESMC checkpoint layout. + """ + + def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: + super().__init__() + self.d_in = d_in + self.eps = eps + self.layer_norm_weight = nn.Parameter(torch.ones(d_in)) + self.layer_norm_bias = nn.Parameter(torch.zeros(d_in)) + self.weight = nn.Parameter(torch.empty(d_out, d_in)) + nn.init.normal_(self.weight, std=0.02) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) + return F.linear(x, self.weight) + + +class _PyTorchLayerNormMLP(nn.Module): + """LayerNorm + SwiGLU MLP. + + Parameters are named ``layer_norm_weight``, ``layer_norm_bias``, + ``fc1_weight`` and ``fc2_weight`` to match the published ESMC checkpoint + layout. + """ + + def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> None: + super().__init__() + self.hidden_size = hidden_size + self.ffn_hidden_size = ffn_hidden_size + self.eps = eps + self.layer_norm_weight = nn.Parameter(torch.ones(hidden_size)) + self.layer_norm_bias = nn.Parameter(torch.zeros(hidden_size)) + self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, hidden_size)) + self.fc2_weight = nn.Parameter(torch.empty(hidden_size, ffn_hidden_size)) + nn.init.normal_(self.fc1_weight, std=0.02) + nn.init.normal_(self.fc2_weight, std=0.02) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = F.layer_norm( + x, + (self.hidden_size,), + self.layer_norm_weight, + self.layer_norm_bias, + self.eps, + ) + x = F.linear(x, self.fc1_weight) + x1, x2 = x.chunk(2, dim=-1) + x = F.silu(x1) * x2 + return F.linear(x, self.fc2_weight) + + +def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: + """LayerNorm + SwiGLU MLP.""" + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + hidden = _swiglu_hidden_dim(expansion_ratio, d_model) + return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) + + +def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: + """LayerNorm + fused QKV projection.""" + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + return _PyTorchLayerNormLinear(d_model, d_model * 3) + + +def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: + """Attention output projection.""" + return nn.Linear(d_model, d_model, bias=bias) + + +def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequential: + hidden = int(expansion_ratio * d_model) + return nn.Sequential( + nn.LayerNorm(d_model), + nn.Linear(d_model, hidden, bias=bias), + nn.GELU(), + nn.Linear(hidden, d_model, bias=bias), + ) + + +# --------------------------------------------------------------------------- +# Attention +# --------------------------------------------------------------------------- + + +class MultiHeadAttention(nn.Module): + """Multi-head self-attention with QK LayerNorm and RoPE. + + Args: + d_model: Model hidden dimension. + n_heads: Number of attention heads. + bias: Whether to use bias in linear layers. + qk_layernorm: Whether to apply LayerNorm to queries and keys before + computing attention scores. + """ + + def __init__( + self, + config: ESMCConfig, + d_model: int, + n_heads: int, + bias: bool = False, + qk_layernorm: bool = True, + ): + super().__init__() + self.config = config + self.d_model = d_model + self.n_heads = n_heads + self.d_head = d_model // n_heads + self.scaling = self.d_head**-0.5 + self.attention_dropout = 0.0 + + assert not bias, "ESMC was trained with bias=False; bias=True not supported" + self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) + self.out_proj = _make_attn_out_proj(d_model, bias) + + if qk_layernorm: + self.q_ln = nn.LayerNorm(d_model, bias=bias) + self.k_ln = nn.LayerNorm(d_model, bias=bias) + else: + self.q_ln = nn.Identity() + self.k_ln = nn.Identity() + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: torch.Tensor | None = None, + position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, + output_attentions: bool = False, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + """Return ``(context, attn_weights)``. + + Attention is computed by the backend selected through + ``config._attn_implementation`` (``eager`` / ``sdpa`` / + ``flash_attention_2`` / ...). QK-LayerNorm and RoPE (via + ``position_embeddings``) are applied here; the backend only computes + ``softmax(QKᵀ)V``. ``attn_weights`` is ``None`` unless the backend + exposes the probabilities — ``output_attentions=True`` forces the + ``eager`` interface so they are observable. + """ + b, s, _ = hidden_states.shape + qkv = self.layernorm_qkv(hidden_states) + q, k, v = torch.chunk(qkv, 3, dim=-1) + q = self.q_ln(q).to(q.dtype) + k = self.k_ln(k).to(q.dtype) + + # (B, S, D) -> (B, H, S, Dh) + q = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + k = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + v = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + + if position_embeddings is not None: + cos, sin = position_embeddings + q, k = apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1) + + attention_interface: Callable = eager_attention_forward + if not output_attentions: + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) + + attn_output, attn_weights = attention_interface( + self, + q, + k, + v, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + attn_output = attn_output.reshape(b, s, -1) + return self.out_proj(attn_output), attn_weights + + +# --------------------------------------------------------------------------- +# Transformer blocks +# --------------------------------------------------------------------------- + + +class UnifiedTransformerBlock(nn.Module): + """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. + + Args: + d_model: Hidden dimension. + n_heads: Number of attention heads. + use_flash_attn: Use Flash Attention 2 kernel if available. + bias: Whether linear layers include bias terms. + expansion_ratio: Hidden-dim expansion ratio for the FFN. + residue_scaling_factor: Scales residual connections to stabilise deep + networks (``1 / sqrt(n_layers / 36)`` is the ESM3 scheme). + qk_layernorm: Whether to apply QK LayerNorm in attention. + ffn_type: Feed-forward activation: ``"swiglu"`` or ``"gelu"``. + """ + + def __init__( + self, + config: ESMCConfig, + d_model: int, + n_heads: int, + bias: bool = False, + expansion_ratio: float = 4.0, + residue_scaling_factor: float = 1.0, + qk_layernorm: bool = True, + ffn_type: str = "swiglu", + ): + super().__init__() + + self.attn = MultiHeadAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + + if ffn_type == "swiglu": + self.ffn = _swiglu_ln_ffn(d_model, expansion_ratio, bias) + elif ffn_type == "gelu": + self.ffn = _gelu_ln_ffn(d_model, expansion_ratio, bias) + else: + raise ValueError(f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'.") + + self.scaling_factor = residue_scaling_factor + + def forward( + self, + x: torch.Tensor, + attention_mask: torch.Tensor | None = None, + position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, + output_attentions: bool = False, + **kwargs, + ) -> tuple[torch.Tensor, torch.Tensor | None]: + """ + Args: + x: ``(batch, seq_len, d_model)`` + attention_mask: Additive attention bias broadcastable to + ``(batch, num_heads, seq_len, seq_len)``, or ``None`` for full + (unmasked) attention. + output_attentions: When ``True``, returns the per-head attention + weights for this block alongside the residual output. + + Returns: + ``(output, attn_weights_or_None)``. Shape of ``output`` is + ``(batch, seq_len, d_model)``; ``attn_weights`` shape is + ``(batch, num_heads, seq_len, seq_len)`` or ``None``. + """ + attn_out, attn_weights = self.attn( + x, + attention_mask, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, + ) + x = x + attn_out / self.scaling_factor + x = x + self.ffn(x) / self.scaling_factor + return x, attn_weights + + +class TransformerStack(nn.Module): + """Stack of :class:`UnifiedTransformerBlock` layers with a final LayerNorm. + + Args: + d_model: Hidden dimension. + n_heads: Number of attention heads. + n_layers: Number of transformer blocks. + scale_residue: When ``True`` apply ESM3 residue scaling + ``sqrt(n_layers / 36)`` to each block. + bias: Bias flag forwarded to every sub-module. + qk_layernorm: QK LayerNorm flag forwarded to every block. + ffn_type: FFN activation type (``"swiglu"`` or ``"gelu"``). + expansion_ratio: FFN expansion ratio. + use_flash_attn: Use Flash Attention 2 kernel when available. + """ + + def __init__( + self, + config: ESMCConfig, + d_model: int, + n_heads: int, + n_layers: int, + scale_residue: bool = True, + bias: bool = False, + qk_layernorm: bool = True, + ffn_type: str = "swiglu", + expansion_ratio: float = 8 / 3, + ): + super().__init__() + self.blocks = nn.ModuleList( + [ + UnifiedTransformerBlock( + config, + d_model, + n_heads, + residue_scaling_factor=math.sqrt(n_layers / 36) if scale_residue else 1.0, + expansion_ratio=expansion_ratio, + bias=bias, + qk_layernorm=qk_layernorm, + ffn_type=ffn_type, + ) + for _ in range(n_layers) + ] + ) + self.norm = nn.LayerNorm(d_model, bias=False) + + def forward( + self, + x: torch.Tensor, + attention_mask: torch.Tensor | None = None, + position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, + layers_to_collect: list[int] | None = None, + output_attentions: bool = False, + **kwargs, + ) -> tuple[ + torch.Tensor, + torch.Tensor, + tuple[torch.Tensor, ...], + tuple[torch.Tensor, ...] | None, + ]: + """Run the full transformer stack. + + Args: + x: ``(batch, seq_len, d_model)`` + attention_mask: Additive attention bias forwarded to each block, or + ``None`` for full attention. + layers_to_collect: Layer indices (0-based pre-block inputs plus + ``n_layers`` for the post-norm output) whose hidden states + should be returned. + output_attentions: When ``True``, collects the per-block attention + weights and returns them as the fourth tuple element. + + Returns: + ``(post_norm, pre_norm, hidden_states, attentions)`` where + ``hidden_states`` is a (possibly empty) tuple of tensors and + ``attentions`` is a tuple of per-block ``(B, H, L, L)`` tensors + or ``None`` when ``output_attentions`` is ``False``. + """ + if layers_to_collect is None: + layers_to_collect = [] + + collected: list[torch.Tensor] = [] + all_attentions: list[torch.Tensor] = [] + for layer_idx, block in enumerate(self.blocks): + if layer_idx in layers_to_collect: + collected.append(x) + x, attn_weights = block( + x, + attention_mask, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, + ) + if output_attentions and attn_weights is not None: + all_attentions.append(attn_weights) + + norm_x = self.norm(x) + if len(self.blocks) in layers_to_collect: + collected.append(norm_x) + + attentions = tuple(all_attentions) if output_attentions else None + return norm_x, x, tuple(collected), attentions + + +# --------------------------------------------------------------------------- +# Pre-trained model base class +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCPreTrainedModel(PreTrainedModel): + """Base class for ESMC models. + + Handles weight initialisation and declares module-level capabilities. + """ + + config_class = ESMCConfig + base_model_prefix = "esmc" + supports_gradient_checkpointing = False + _supports_sdpa = True + _supports_flash_attn = True + _supports_attention_backend = True + _no_split_modules = ["UnifiedTransformerBlock"] + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + + def _init_weights(self, module: nn.Module): + std = self.config.initializer_range + if isinstance(module, nn.Linear): + module.weight.data.normal_(mean=0.0, std=std) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, ESMCRotaryEmbedding): + module.inv_freq = module._compute_inv_freq(module.config, device=self.device) + + +# --------------------------------------------------------------------------- +# Base encoder model +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCModel(ESMCPreTrainedModel): + """The bare ESMC encoder outputting raw hidden states. + + ESMC is a protein language model trained by EvolutionaryScale using a + masked-token objective over amino acid sequences. The architecture is a + standard Transformer encoder with RoPE positional embeddings, QK LayerNorm, + and SwiGLU feed-forward networks. + + Args: + config: An :class:`ESMCConfig` instance. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.embed = nn.Embedding(config.vocab_size, config.d_model) + self.rotary_emb = ESMCRotaryEmbedding(config) + self.transformer = TransformerStack( + config, + config.d_model, + config.n_heads, + config.n_layers, + ) + self._sae_models: nn.ModuleDict = nn.ModuleDict() + self.post_init() + + def get_input_embeddings(self) -> nn.Embedding: + return self.embed + + def set_input_embeddings(self, value: nn.Embedding): + self.embed = value + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Register one or more SAEs obtained from an :class:`ESMCSAEModel`. + + Each is keyed by ``f"layer{N}"`` (the backbone-layer index ``N`` the + SAE is trained against, set by + :meth:`ESMCSAEModel.initialize_layers`). Attaching two SAEs for the + same backbone layer raises — only one SAE per layer can be active. + + Example:: + + sae = ESMCSAEModel.from_pretrained( + "biohub/esmc-600m-2024-12-sae-k64-codebook16384" + ) + sae.initialize_layers([27, 33]) + model.add_sae_models([sae.layers["27"], sae.layers["33"]]) + """ + for layer in sae_models: + assert isinstance(layer, _ESMCSAELayer), ( + f"Expected an SAE layer (model.layers['']), got {type(layer).__name__}." + ) + key = f"layer{int(layer.layer)}" + if key in self._sae_models: + raise ValueError( + f"An SAE is already registered at {key!r}. Only one SAE " + "per backbone layer can be active — pick a different " + "layer on one of them, or attach in a fresh model." + ) + self._sae_models[key] = layer + + _SAE_KEY_RE = re.compile(r"layer(\d+)") + + def _get_sae_layer_num_requested(self, model_name: str) -> int: + """Recover the backbone-layer index from a key written by + :meth:`add_sae_models` (``"layer{N}"`` → ``N``).""" + match = self._SAE_KEY_RE.fullmatch(model_name) + assert match is not None, f"Unexpected SAE key {model_name!r}; expected 'layer{{N}}'." + return int(match.group(1)) + + def _validate_sae_inputs(self, input_ids: torch.Tensor) -> None: + assert torch.all(input_ids != self.config.mask_token_id), ( + "SAE inputs must not contain mask tokens. SAEs were trained on unmasked sequences." + ) + + def _get_sae_outputs( + self, + hidden_states: torch.Tensor, + layers_to_collect: list[int], + token_mask: torch.Tensor, + normalize_sae: bool = False, + ) -> dict[str, torch.Tensor]: + """Run all registered SAEs and return their feature magnitudes. + + Args: + hidden_states: Stacked tensor of shape + ``(len(layers_to_collect), batch, seq_len, d_model)``. + layers_to_collect: The ESMC layer indices that were collected, + in the same order as the first dim of ``hidden_states``. + token_mask: Boolean mask ``(batch, seq_len)`` — ``True`` for + real (non-padding) tokens. + normalize_sae: When ``True``, scale features by ``idf / max`` + using the per-feature stats trained alongside each SAE. + """ + layer_to_idx = {layer: idx for idx, layer in enumerate(layers_to_collect)} + sae_outputs: dict[str, torch.Tensor] = {} + + for model_name, sae_module in self._sae_models.items(): + # `nn.ModuleDict` only stores `nn.Module`s at the type level; + # ``add_sae_models`` enforces that each entry is an ``_ESMCSAELayer``. + assert isinstance(sae_module, _ESMCSAELayer) + layer: _ESMCSAELayer = sae_module + requested_layer = self._get_sae_layer_num_requested(model_name) + layer_idx = layer_to_idx[requested_layer] + layer_states = hidden_states[layer_idx].clone().to(self.device) + + sae_out = layer.get_sae_output(layer_states, token_mask) + features = sae_out.feature_magnitudes.detach() + + if normalize_sae: + # ``register_buffer`` is typed as ``Tensor | Module`` on + # ``nn.Module``; narrow here since these are Tensors. + idf = cast(torch.Tensor, layer.idf) + max_val = cast(torch.Tensor, layer.max) + features = (features / max_val) * idf + + sae_outputs[model_name] = features.to_sparse() + + return sae_outputs + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + sequence_id: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCOutput: + r""" + sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Integer chain-ID tensor for chain-aware attention masking. Tokens with the same + non-negative integer value can attend to each other; tokens with different values + cannot (cross-chain masking). Padding positions should be set to ``-1``. + When provided, ``attention_mask`` is ignored. The ``flash_attention_2`` backend + only supports single-chain inputs (all non-padding values must be ``0``); pass + multi-chain ``sequence_id`` with ``attn_implementation='sdpa'`` (or ``'eager'``). + output_attentions (`bool`, *optional*): + Whether to return the per-block attention weights of shape + ``(batch_size, num_heads, sequence_length, sequence_length)``. + Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the + attention probabilities are observable; raises on the + ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run any SAE models registered via :meth:`add_sae_models`. + Has no effect when no SAEs are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE feature magnitudes by ``idf / max`` (only + applied when the SAE's normalization buffers contain non-trivial values). + + Examples: + + ```python + >>> from transformers import AutoTokenizer, ESMCModel + + >>> model = ESMCModel.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> inputs = tokenizer(["MLKNVQVQLV"], return_tensors="pt") + >>> outputs = model(**inputs) + >>> outputs.last_hidden_state.shape + torch.Size([1, 12, 960]) + ``` + """ + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + output_sae = compute_sae and len(self._sae_models) > 0 + + # Determine which intermediate layers to collect. When SAEs are + # registered we must collect at least the layers they target, even if + # the caller did not ask for all hidden states. + if output_hidden_states: + layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) + elif output_sae: + layers_to_collect = sorted({self._get_sae_layer_num_requested(name) for name in self._sae_models}) + else: + layers_to_collect = [] + + user_supplied_sequence_id = sequence_id is not None + if user_supplied_sequence_id: + bool_mask = sequence_id >= 0 + else: + if attention_mask is None: + attention_mask = input_ids != self.config.pad_token_id + assert attention_mask is not None + bool_mask = attention_mask.bool() + + x = self.embed(input_ids) + position_ids = torch.arange(x.shape[1], device=x.device).unsqueeze(0) + position_embeddings = self.rotary_emb(x, position_ids) + + if user_supplied_sequence_id: + if self.config._attn_implementation == "flash_attention_2" and (sequence_id > 0).any(): + raise ValueError( + "Multi-chain ``sequence_id`` (any value > 0) is not " + "supported with attn_implementation='flash_attention_2'. " + "Re-load the model with attn_implementation='sdpa' (or " + "'eager') for chain-aware attention masking." + ) + # Block-diagonal chain mask: a token attends only to tokens sharing + # its ``sequence_id``. Additive bias broadcast over heads, shape + # ``(batch, 1, seq_len, seq_len)``; handled by the eager / sdpa paths. + same_chain = sequence_id.unsqueeze(-1) == sequence_id.unsqueeze(-2) + attn_bias = torch.zeros(same_chain.shape, dtype=x.dtype, device=x.device).masked_fill_( + ~same_chain, torch.finfo(x.dtype).min + ) + attn_bias = attn_bias.unsqueeze(1) + else: + attn_bias = create_bidirectional_mask( + config=self.config, + inputs_embeds=x, + attention_mask=attention_mask, + ) + + last_hidden_state, _, collected, attentions = self.transformer( + x, + attention_mask=attn_bias, + position_embeddings=position_embeddings, + layers_to_collect=layers_to_collect, + output_attentions=output_attentions, + ) + + # Stack once; reused for both SAE and hidden-state output. + collected_tensor: torch.Tensor | None = ( + torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] + ) + + sae_outputs: dict[str, torch.Tensor] | None = None + if output_sae and collected_tensor is not None: + assert input_ids is not None + self._validate_sae_inputs(input_ids) + sae_outputs = self._get_sae_outputs(collected_tensor, layers_to_collect, bool_mask, normalize_sae) + + hidden_states_tensor = collected_tensor if output_hidden_states else None + + if not return_dict: + return tuple( + v + for v in [ + last_hidden_state, + hidden_states_tensor, + sae_outputs, + attentions, + ] + if v is not None + ) + + return ESMCOutput( + last_hidden_state=last_hidden_state, + hidden_states=hidden_states_tensor, + sae_outputs=sae_outputs, + attentions=attentions, + ) + + +# --------------------------------------------------------------------------- +# LM head +# --------------------------------------------------------------------------- + + +def _esmc_lm_head(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Sequential: + """Linear → GELU → LayerNorm → Linear projection head for masked LM.""" + hidden_dim = hidden_dim if hidden_dim is not None else d_model + return nn.Sequential( + nn.Linear(d_model, hidden_dim), + nn.GELU(), + nn.LayerNorm(hidden_dim), + nn.Linear(hidden_dim, output_dim), + ) + + +# --------------------------------------------------------------------------- +# Masked language model +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCForMaskedLM(ESMCPreTrainedModel): + """ESMC with a masked language modelling head. + + This is the primary pre-training objective of ESMC. The LM head consists + of a single hidden layer with GELU activation followed by LayerNorm and a + linear projection to ``vocab_size``. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.esmc = ESMCModel(config) + self.lm_head = _esmc_lm_head(config.d_model, config.vocab_size) + self.post_init() + + def get_output_embeddings(self) -> nn.Linear: + return self.lm_head[-1] # type: ignore[return-value] + + def set_output_embeddings(self, new_embeddings: nn.Linear): + self.lm_head[-1] = new_embeddings + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Proxy to :meth:`ESMCModel.add_sae_models`.""" + self.esmc.add_sae_models(sae_models) + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + sequence_id: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + labels: torch.Tensor | None = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: + r""" + sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Integer chain-ID tensor forwarded to the encoder for chain-aware + attention masking. See :meth:`ESMCModel.forward` for the encoding. + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for masked language modelling loss. Positions with label ``-100`` + are ignored. Other positions must be in ``[0, config.vocab_size)``. + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run registered SAE models. Has no effect when none are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE features by ``idf / max`` normalization buffers. + + Examples: + + ```python + >>> from transformers import AutoTokenizer, ESMCForMaskedLM + >>> import torch + + >>> model = ESMCForMaskedLM.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> inputs = tokenizer(["MLKNVQLV"], return_tensors="pt") + >>> outputs = model(**inputs) + >>> outputs.logits.shape + torch.Size([1, 11, 64]) + ``` + """ + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + encoder_outputs = self.esmc( + input_ids=input_ids, + attention_mask=attention_mask, + sequence_id=sequence_id, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=True, + compute_sae=compute_sae, + normalize_sae=normalize_sae, + ) + + logits = self.lm_head(encoder_outputs.last_hidden_state) + + loss: torch.Tensor | None = None + if labels is not None: + loss = CrossEntropyLoss(ignore_index=-100)(logits.view(-1, self.config.vocab_size), labels.view(-1)) + + if not return_dict: + return tuple( + v + for v in [ + loss, + logits, + encoder_outputs.last_hidden_state, + encoder_outputs.hidden_states, + encoder_outputs.sae_outputs, + encoder_outputs.attentions, + ] + if v is not None + ) + + return ESMCMaskedLMOutput( + loss=loss, + logits=logits, + last_hidden_state=encoder_outputs.last_hidden_state, + hidden_states=encoder_outputs.hidden_states, + sae_outputs=encoder_outputs.sae_outputs, + attentions=encoder_outputs.attentions, + ) + + +# --------------------------------------------------------------------------- +# Classification heads +# --------------------------------------------------------------------------- + + +class _ESMCClassificationHead(nn.Module): + """Dense classification head applied to the ```` token representation.""" + + def __init__(self, config: ESMCConfig): + super().__init__() + self.dense = nn.Linear(config.d_model, config.d_model) + self.dropout = nn.Dropout(config.classifier_dropout) + self.out_proj = nn.Linear(config.d_model, config.num_labels) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + x = hidden_states[:, 0, :] # token + x = self.dropout(x) + x = torch.tanh(self.dense(x)) + x = self.dropout(x) + return self.out_proj(x) + + +# --------------------------------------------------------------------------- +# Sequence classification +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCForSequenceClassification(ESMCPreTrainedModel): + """ESMC with a sequence-level classification head. + + A linear layer is applied to the ```` token representation. + Supports regression (``num_labels == 1``), single-label classification, + and multi-label classification. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.num_labels = config.num_labels + self.esmc = ESMCModel(config) + self.classifier = _ESMCClassificationHead(config) + self.post_init() + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Proxy to :meth:`ESMCModel.add_sae_models`.""" + self.esmc.add_sae_models(sae_models) + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: torch.LongTensor | None = None, + attention_mask: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + labels: torch.Tensor | None = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: + r""" + labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for sequence classification loss. Indices must be in + ``[0, config.num_labels - 1]``. For regression pass a float + tensor of shape ``(batch_size,)``. + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run registered SAE models. Has no effect when none are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE features by ``idf / max`` normalization buffers. + """ + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + encoder_outputs = self.esmc( + input_ids, + attention_mask=attention_mask, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=True, + compute_sae=compute_sae, + normalize_sae=normalize_sae, + ) + logits = self.classifier(encoder_outputs.last_hidden_state) + + loss: torch.Tensor | None = None + if labels is not None: + labels = labels.to(logits.device) + + if self.config.problem_type is None: + if self.num_labels == 1: + self.config.problem_type = "regression" + elif self.num_labels > 1 and labels.dtype in (torch.long, torch.int): + self.config.problem_type = "single_label_classification" + else: + self.config.problem_type = "multi_label_classification" + + if self.config.problem_type == "regression": + loss_fct = MSELoss() + loss = loss_fct( + logits.squeeze() if self.num_labels == 1 else logits, + labels.squeeze() if self.num_labels == 1 else labels, + ) + elif self.config.problem_type == "single_label_classification": + loss = CrossEntropyLoss()(logits.view(-1, self.num_labels), labels.view(-1)) + elif self.config.problem_type == "multi_label_classification": + loss = BCEWithLogitsLoss()(logits, labels) + + if not return_dict: + return tuple( + v + for v in [ + loss, + logits, + encoder_outputs.last_hidden_state, + encoder_outputs.hidden_states, + encoder_outputs.sae_outputs, + encoder_outputs.attentions, + ] + if v is not None + ) + + return ESMCSequenceClassifierOutput( + loss=loss, + logits=logits, + last_hidden_state=encoder_outputs.last_hidden_state, + hidden_states=encoder_outputs.hidden_states, + sae_outputs=encoder_outputs.sae_outputs, + attentions=encoder_outputs.attentions, + ) + + +# --------------------------------------------------------------------------- +# Token classification +# --------------------------------------------------------------------------- + + +@auto_docstring +class ESMCForTokenClassification(ESMCPreTrainedModel): + """ESMC with a per-token classification head. + + Useful for tasks such as secondary structure prediction, contact-map + prediction, or per-residue labelling. + """ + + def __init__(self, config: ESMCConfig): + super().__init__(config) + self.num_labels = config.num_labels + self.esmc = ESMCModel(config) + self.dropout = nn.Dropout(config.classifier_dropout) + self.classifier = nn.Linear(config.d_model, config.num_labels) + self.post_init() + + def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: + """Proxy to :meth:`ESMCModel.add_sae_models`.""" + self.esmc.add_sae_models(sae_models) + + @can_return_tuple + @auto_docstring + def forward( + self, + input_ids: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + output_hidden_states: bool | None = None, + output_attentions: bool | None = None, + return_dict: bool | None = None, + labels: torch.Tensor | None = None, + compute_sae: bool = True, + normalize_sae: bool = False, + ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. + Positions with index ``-100`` are ignored in the loss. + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. + compute_sae (`bool`, *optional*, defaults to ``True``): + Whether to run registered SAE models. Has no effect when none are registered. + normalize_sae (`bool`, *optional*, defaults to ``False``): + When ``True``, scale SAE features by ``idf / max`` normalization buffers. + """ + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + encoder_outputs = self.esmc( + input_ids=input_ids, + attention_mask=attention_mask, + output_hidden_states=output_hidden_states, + output_attentions=output_attentions, + return_dict=True, + compute_sae=compute_sae, + normalize_sae=normalize_sae, + ) + + sequence_output = self.dropout(encoder_outputs.last_hidden_state) + logits = self.classifier(sequence_output) + + loss: torch.Tensor | None = None + if labels is not None: + loss = CrossEntropyLoss(ignore_index=-100)( + logits.view(-1, self.num_labels), labels.to(logits.device).view(-1) + ) + + if not return_dict: + return tuple( + v + for v in [ + loss, + logits, + encoder_outputs.last_hidden_state, + encoder_outputs.hidden_states, + encoder_outputs.sae_outputs, + encoder_outputs.attentions, + ] + if v is not None + ) + + return ESMCTokenClassifierOutput( + loss=loss, + logits=logits, + last_hidden_state=encoder_outputs.last_hidden_state, + hidden_states=encoder_outputs.hidden_states, + sae_outputs=encoder_outputs.sae_outputs, + attentions=encoder_outputs.attentions, + ) + + +__all__ = [ + "ESMCModel", + "ESMCForMaskedLM", + "ESMCForSequenceClassification", + "ESMCForTokenClassification", + "ESMCPreTrainedModel", +] From a1d91a7a1083c03ae2b4a282180b87272ee85aa5 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 14:33:11 +0100 Subject: [PATCH 08/70] Fix ESMC checkpoint loading + bidirectional attention (verified vs fork on ESMC-6B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs surfaced only when loading the real biohub/ESMC-6B checkpoint and running an unpadded sequence (prior parity tests all used padded inputs): 1. _init_weights re-initialized loaded nn.Linear weights. It used `module.weight.data.normal_()`, which writes through `.data` and bypasses the `_is_hf_initialized` flag that transformers sets on loaded params. So `from_pretrained` clobbered out_proj / lm_head with random init (silently — no missing-keys warning), while custom/LayerNorm/Embedding modules survived only because _init_weights had no branch for them. Switch to the flag-respecting `transformers.initialization` helpers (init.normal_, init.zeros_, init.copy_), matching esm/base. (This bug is latent in the v4 fork too; it only bites under the v5 init-after-load flow.) 2. Attention defaulted to causal on unpadded inputs. ESMC is a bidirectional encoder, but MultiHeadAttention never set `is_causal`, so the sdpa/flash interface fell back to `getattr(module, "is_causal", True)` and applied causal masking whenever `attention_mask is None`. Set `self.is_causal = False`. (Introduced by the attention-interface refactor; missed because every earlier parity test passed a padded 4D mask.) Source of truth is modular_esmc.py; modeling_esmc.py regenerated. Verified: loading biohub/ESMC-6B (and ESMC-300M) into the refactored model and into the original fork code yields BIT-IDENTICAL logits (max|Δ| = 0.0) and identical argmax predictions on an 80-residue sequence. State-dict loads with no remapping (240 TE `_extra_state` keys ignored as before). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/modeling_esmc.py | 11 ++++++++--- src/transformers/models/esmc/modular_esmc.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 010619aed833..3c75e8ee1afd 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -29,6 +29,7 @@ from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss from torch.nn import functional as F +from ... import initialization as init from ...integrations import use_kernel_func_from_hub from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] @@ -361,6 +362,10 @@ def __init__( self.d_head = d_model // n_heads self.scaling = self.d_head**-0.5 self.attention_dropout = 0.0 + # ESMC is a bidirectional encoder: never apply causal masking. Without + # this, the sdpa/flash interfaces default `is_causal` to True when no + # attention_mask is passed (unpadded inputs), silently masking causally. + self.is_causal = False assert not bias, "ESMC was trained with bias=False; bias=True not supported" self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) @@ -659,11 +664,11 @@ class ESMCPreTrainedModel(PreTrainedModel): def _init_weights(self, module: nn.Module): std = self.config.initializer_range if isinstance(module, nn.Linear): - module.weight.data.normal_(mean=0.0, std=std) + init.normal_(module.weight, mean=0.0, std=std) if module.bias is not None: - module.bias.data.zero_() + init.zeros_(module.bias) elif isinstance(module, ESMCRotaryEmbedding): - module.inv_freq = module._compute_inv_freq(module.config, device=self.device) + init.copy_(module.inv_freq, module._compute_inv_freq(module.config)) # --------------------------------------------------------------------------- diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index e439a5b2c64b..2d14d2ada113 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -24,6 +24,7 @@ from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss from torch.nn import functional as F +from ... import initialization as init from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] MaskedLMOutput, @@ -331,6 +332,10 @@ def __init__( self.d_head = d_model // n_heads self.scaling = self.d_head**-0.5 self.attention_dropout = 0.0 + # ESMC is a bidirectional encoder: never apply causal masking. Without + # this, the sdpa/flash interfaces default `is_causal` to True when no + # attention_mask is passed (unpadded inputs), silently masking causally. + self.is_causal = False assert not bias, "ESMC was trained with bias=False; bias=True not supported" self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) @@ -602,11 +607,11 @@ class ESMCPreTrainedModel(PreTrainedModel): def _init_weights(self, module: nn.Module): std = self.config.initializer_range if isinstance(module, nn.Linear): - module.weight.data.normal_(mean=0.0, std=std) + init.normal_(module.weight, mean=0.0, std=std) if module.bias is not None: - module.bias.data.zero_() + init.zeros_(module.bias) elif isinstance(module, ESMCRotaryEmbedding): - module.inv_freq = module._compute_inv_freq(module.config, device=self.device) + init.copy_(module.inv_freq, module._compute_inv_freq(module.config)) # --------------------------------------------------------------------------- From 091ec18c1f5a301e6ba362422af90f79c80f894d Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 14:43:29 +0100 Subject: [PATCH 09/70] Fix ESMCTokenizer docstring example + add tokenizer tests The ESMCTokenizer behaves correctly under v5 (verified against the published biohub/ESMC-6B tokenizer.json), but its docstring doctest example was wrong: it listed 21 ids for a 20-residue sequence with two residues mis-ordered/ dropped. Correct it to the actual output (22 ids, ... ). Add tests/models/esmc/test_tokenization_esmc.py (fast-only tokenizer, so it mirrors models/esm's plain-TestCase style rather than the slow vocab-file setup): documented example, character-level tokenize, / wrapping, special-token ids (incl. bos==cls alias), batch padding + attention_mask, chain-break token, mask token, unknown-residue -> , decode round-trip, save/load round-trip, and a @slow integration test asserting AutoTokenizer resolves to ESMCTokenizer and the hub tokenizer matches the code-built one. Verified: 10 passed, 1 slow-skipped (the slow test passes with RUN_SLOW=1). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmc/tokenization_esmc.py | 3 +- tests/models/esmc/__init__.py | 0 tests/models/esmc/test_tokenization_esmc.py | 104 ++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tests/models/esmc/__init__.py create mode 100644 tests/models/esmc/test_tokenization_esmc.py diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index 248577919450..0e2538500db8 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -20,6 +20,7 @@ from ...tokenization_utils_fast import PreTrainedTokenizerFast # type: ignore[import] from ...utils import logging # type: ignore[import] + logger = logging.get_logger(__name__) VOCAB_FILES_NAMES = {"vocab_file": "vocab.txt", "tokenizer_file": "tokenizer.json"} @@ -92,7 +93,7 @@ class ESMCTokenizer(PreTrainedTokenizerFast): >>> tokenizer = ESMCTokenizer() >>> tokenizer("ACDEFGHIKLMNPQRSTVWY")["input_ids"] - [0, 5, 23, 13, 18, 9, 6, 21, 12, 15, 20, 17, 14, 16, 10, 8, 11, 7, 22, 19, 2] + [0, 5, 23, 13, 9, 18, 6, 21, 12, 15, 4, 20, 17, 14, 16, 10, 8, 11, 7, 22, 19, 2] ``` """ diff --git a/tests/models/esmc/__init__.py b/tests/models/esmc/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/models/esmc/test_tokenization_esmc.py b/tests/models/esmc/test_tokenization_esmc.py new file mode 100644 index 000000000000..1f5ecc21693b --- /dev/null +++ b/tests/models/esmc/test_tokenization_esmc.py @@ -0,0 +1,104 @@ +# Copyright 2026 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tempfile +import unittest + +from transformers import AutoTokenizer, ESMCTokenizer +from transformers.testing_utils import require_tokenizers, slow + + +@require_tokenizers +class ESMCTokenizationTest(unittest.TestCase): + tokenizer_class = ESMCTokenizer + + def get_tokenizer(self, **kwargs) -> ESMCTokenizer: + # ESMC is a fast-only tokenizer; the vocab is built in __init__ (no vocab file needed). + return ESMCTokenizer(**kwargs) + + def test_documented_example(self): + tokenizer = self.get_tokenizer() + # 20-residue sequence -> 20 residues wrapped in ... = 22 ids. + ids = tokenizer("ACDEFGHIKLMNPQRSTVWY")["input_ids"] + self.assertListEqual( + ids, + [0, 5, 23, 13, 9, 18, 6, 21, 12, 15, 4, 20, 17, 14, 16, 10, 8, 11, 7, 22, 19, 2], + ) + + def test_tokenize_is_character_level(self): + tokenizer = self.get_tokenizer() + self.assertListEqual(tokenizer.tokenize("LAGVS"), ["L", "A", "G", "V", "S"]) + self.assertListEqual(tokenizer.convert_tokens_to_ids(["L", "A", "G", "V", "S"]), [4, 5, 6, 7, 8]) + + def test_encode_wraps_cls_eos(self): + tokenizer = self.get_tokenizer() + self.assertListEqual(tokenizer.encode("LAGVS"), [0, 4, 5, 6, 7, 8, 2]) + + def test_special_token_ids(self): + tokenizer = self.get_tokenizer() + self.assertEqual(tokenizer.cls_token_id, 0) + self.assertEqual(tokenizer.pad_token_id, 1) + self.assertEqual(tokenizer.eos_token_id, 2) + self.assertEqual(tokenizer.unk_token_id, 3) + self.assertEqual(tokenizer.mask_token_id, 32) + # ESMC uses as the sequence-start token; it is aliased to bos. + self.assertEqual(tokenizer.bos_token_id, tokenizer.cls_token_id) + self.assertEqual(tokenizer.vocab_size, 33) + + def test_batch_padding(self): + tokenizer = self.get_tokenizer() + batch = tokenizer(["LAGVS", "WC"], padding=True) + self.assertListEqual(batch["input_ids"][0], [0, 4, 5, 6, 7, 8, 2]) + self.assertListEqual(batch["input_ids"][1], [0, 22, 23, 2, 1, 1, 1]) + self.assertListEqual(batch["attention_mask"][1], [1, 1, 1, 1, 0, 0, 0]) + + def test_chain_break_token(self): + tokenizer = self.get_tokenizer() + self.assertEqual(tokenizer.chain_break_token, "|") + ids = tokenizer("MK|AY")["input_ids"] + self.assertIn(tokenizer.chain_break_token_id, ids) + self.assertEqual(tokenizer.chain_break_token_id, 31) + + def test_mask_token(self): + tokenizer = self.get_tokenizer() + self.assertIn(tokenizer.mask_token_id, tokenizer("MKT")["input_ids"]) + + def test_unknown_residue_maps_to_unk(self): + tokenizer = self.get_tokenizer() + # "J" is not a valid amino-acid token in the ESMC vocabulary. + self.assertIn(tokenizer.unk_token_id, tokenizer("MKJT")["input_ids"]) + + def test_decode_round_trip(self): + tokenizer = self.get_tokenizer() + seq = "MKTAYIAKQR" + decoded = tokenizer.decode(tokenizer(seq)["input_ids"], skip_special_tokens=True).replace(" ", "") + self.assertEqual(decoded, seq) + + def test_save_and_load(self): + tokenizer = self.get_tokenizer() + seq = "MKTAYIAKQR" + with tempfile.TemporaryDirectory() as tmpdir: + tokenizer.save_pretrained(tmpdir) + reloaded = ESMCTokenizer.from_pretrained(tmpdir) + self.assertListEqual(tokenizer(seq)["input_ids"], reloaded(seq)["input_ids"]) + + @slow + def test_tokenizer_integration(self): + # The published checkpoint's tokenizer.json must match the code-built tokenizer, + # and AutoTokenizer must resolve to ESMCTokenizer. + seq = "ACDEFGHIKLMNPQRSTVWY" + built = self.get_tokenizer() + auto = AutoTokenizer.from_pretrained("biohub/ESMC-6B") + self.assertIsInstance(auto, ESMCTokenizer) + self.assertListEqual(built(seq)["input_ids"], auto(seq)["input_ids"]) From cce0178a0c0591d9cabafe229adb981e01337e52 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 14:57:45 +0100 Subject: [PATCH 10/70] Drop vestigial vocab_file entry from ESMCTokenizer VOCAB_FILES_NAMES ESMCTokenizer is fast-only (built from a tokenizer object); it never reads or writes a slow `vocab.txt`. Declare only `tokenizer_file: tokenizer.json`. save_pretrained already emits just tokenizer.json + tokenizer_config.json; tokenizer tests still pass (10 passed, 1 slow-skipped). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/tokenization_esmc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index 0e2538500db8..e5fb07813f2f 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -23,7 +23,7 @@ logger = logging.get_logger(__name__) -VOCAB_FILES_NAMES = {"vocab_file": "vocab.txt", "tokenizer_file": "tokenizer.json"} +VOCAB_FILES_NAMES = {"tokenizer_file": "tokenizer.json"} # Canonical amino acid vocabulary used by all ESMC checkpoints. # Indices must be kept stable — they are hard-coded into model weights. From 33afd3843e4bd3f5ea89d100f3b24eafe2b4f300 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 14:57:45 +0100 Subject: [PATCH 11/70] Add ESMC model tests + make _init_weights cover all modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests/models/esmc/test_modeling_esmc.py (ModelTesterMixin + PipelineTesterMixin), mirroring models/esm: ESMCModelTester + create_and_check for ESMCModel / ForMaskedLM / ForSequenceClassification / ForTokenClassification, plus a @slow masked-LM integration test on biohub/ESMC-300M. ESMCModel has no pooler, so the base-model check asserts only last_hidden_state. Fix surfaced by test_can_init_all_missing_weights: _init_weights only handled nn.Linear, so the fused-LN modules, nn.LayerNorm and nn.Embedding were left uninitialized on a from-scratch / meta-device init. Handle the custom _PyTorchLayerNormLinear / _PyTorchLayerNormMLP explicitly (their `weight` is a Linear weight, not a norm — the base initializer matches norms by class-name substring and would wrongly set it to ones), the rotary buffer explicitly, and delegate nn.Linear / nn.Embedding / nn.LayerNorm to super()._init_weights. Skip test_retain_grad_hidden_states_attentions: ESMC returns `hidden_states` as one stacked tensor (consumed by the SAE feature), not the live per-layer tensors, so grad cannot flow back to the returned copy — intentional. Verified: full model test file = 92 passed, 102 skipped, 0 failed; the @slow integration test passes; and from_pretrained still loads biohub/ESMC weights bit-exactly (the flag-respecting init helpers don't clobber loaded params). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/modeling_esmc.py | 17 +- src/transformers/models/esmc/modular_esmc.py | 17 +- tests/models/esmc/test_modeling_esmc.py | 210 ++++++++++++++++++ 3 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 tests/models/esmc/test_modeling_esmc.py diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 3c75e8ee1afd..38f55f19071b 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -663,12 +663,23 @@ class ESMCPreTrainedModel(PreTrainedModel): def _init_weights(self, module: nn.Module): std = self.config.initializer_range - if isinstance(module, nn.Linear): + # The fused LN+projection modules are handled explicitly: the base + # `_init_weights` matches norms by class-name substring ("LayerNorm"), + # which would otherwise mis-initialize their (Linear) `weight` to ones. + if isinstance(module, _PyTorchLayerNormLinear): + init.ones_(module.layer_norm_weight) + init.zeros_(module.layer_norm_bias) init.normal_(module.weight, mean=0.0, std=std) - if module.bias is not None: - init.zeros_(module.bias) + elif isinstance(module, _PyTorchLayerNormMLP): + init.ones_(module.layer_norm_weight) + init.zeros_(module.layer_norm_bias) + init.normal_(module.fc1_weight, mean=0.0, std=std) + init.normal_(module.fc2_weight, mean=0.0, std=std) elif isinstance(module, ESMCRotaryEmbedding): init.copy_(module.inv_freq, module._compute_inv_freq(module.config)) + else: + # nn.Linear / nn.Embedding / nn.LayerNorm via the base initializer. + super()._init_weights(module) # --------------------------------------------------------------------------- diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 2d14d2ada113..5560f1eacdce 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -606,12 +606,23 @@ class ESMCPreTrainedModel(PreTrainedModel): def _init_weights(self, module: nn.Module): std = self.config.initializer_range - if isinstance(module, nn.Linear): + # The fused LN+projection modules are handled explicitly: the base + # `_init_weights` matches norms by class-name substring ("LayerNorm"), + # which would otherwise mis-initialize their (Linear) `weight` to ones. + if isinstance(module, _PyTorchLayerNormLinear): + init.ones_(module.layer_norm_weight) + init.zeros_(module.layer_norm_bias) init.normal_(module.weight, mean=0.0, std=std) - if module.bias is not None: - init.zeros_(module.bias) + elif isinstance(module, _PyTorchLayerNormMLP): + init.ones_(module.layer_norm_weight) + init.zeros_(module.layer_norm_bias) + init.normal_(module.fc1_weight, mean=0.0, std=std) + init.normal_(module.fc2_weight, mean=0.0, std=std) elif isinstance(module, ESMCRotaryEmbedding): init.copy_(module.inv_freq, module._compute_inv_freq(module.config)) + else: + # nn.Linear / nn.Embedding / nn.LayerNorm via the base initializer. + super()._init_weights(module) # --------------------------------------------------------------------------- diff --git a/tests/models/esmc/test_modeling_esmc.py b/tests/models/esmc/test_modeling_esmc.py new file mode 100644 index 000000000000..40b6de49bf48 --- /dev/null +++ b/tests/models/esmc/test_modeling_esmc.py @@ -0,0 +1,210 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Testing suite for the PyTorch ESMC model.""" + +import unittest + +from transformers import ESMCConfig, is_torch_available +from transformers.testing_utils import require_torch, slow, torch_device + +from ...test_configuration_common import ConfigTester +from ...test_modeling_common import ModelTesterMixin, ids_tensor, random_attention_mask +from ...test_pipeline_mixin import PipelineTesterMixin + + +if is_torch_available(): + import torch + + from transformers import ( + ESMCForMaskedLM, + ESMCForSequenceClassification, + ESMCForTokenClassification, + ESMCModel, + ) + + +class ESMCModelTester: + def __init__( + self, + parent, + batch_size=13, + seq_length=7, + is_training=False, + use_input_mask=True, + use_labels=True, + vocab_size=33, + d_model=32, + n_layers=2, + n_heads=4, + initializer_range=0.02, + num_labels=3, + scope=None, + ): + self.parent = parent + self.batch_size = batch_size + self.seq_length = seq_length + self.is_training = is_training + self.use_input_mask = use_input_mask + self.use_labels = use_labels + self.vocab_size = vocab_size + self.d_model = d_model + self.n_layers = n_layers + self.n_heads = n_heads + self.initializer_range = initializer_range + self.num_labels = num_labels + self.scope = scope + # aliases consumed by ModelTesterMixin + self.hidden_size = d_model + self.num_hidden_layers = n_layers + self.num_attention_heads = n_heads + + def prepare_config_and_inputs(self): + input_ids = ids_tensor([self.batch_size, self.seq_length], self.vocab_size) + + input_mask = None + if self.use_input_mask: + input_mask = random_attention_mask([self.batch_size, self.seq_length]) + + sequence_labels = None + token_labels = None + if self.use_labels: + sequence_labels = ids_tensor([self.batch_size], self.num_labels) + token_labels = ids_tensor([self.batch_size, self.seq_length], self.num_labels) + + config = self.get_config() + return config, input_ids, input_mask, sequence_labels, token_labels + + def get_config(self): + return ESMCConfig( + vocab_size=self.vocab_size, + d_model=self.d_model, + n_heads=self.n_heads, + n_layers=self.n_layers, + pad_token_id=1, + initializer_range=self.initializer_range, + num_labels=self.num_labels, + ) + + def create_and_check_model(self, config, input_ids, input_mask, sequence_labels, token_labels): + model = ESMCModel(config=config) + model.to(torch_device) + model.eval() + result = model(input_ids, attention_mask=input_mask) + result = model(input_ids) + self.parent.assertEqual(result.last_hidden_state.shape, (self.batch_size, self.seq_length, self.d_model)) + + def create_and_check_for_masked_lm(self, config, input_ids, input_mask, sequence_labels, token_labels): + model = ESMCForMaskedLM(config=config) + model.to(torch_device) + model.eval() + result = model(input_ids, attention_mask=input_mask, labels=token_labels) + self.parent.assertEqual(result.logits.shape, (self.batch_size, self.seq_length, self.vocab_size)) + + def create_and_check_for_sequence_classification( + self, config, input_ids, input_mask, sequence_labels, token_labels + ): + config.num_labels = self.num_labels + model = ESMCForSequenceClassification(config=config) + model.to(torch_device) + model.eval() + result = model(input_ids, attention_mask=input_mask, labels=sequence_labels) + self.parent.assertEqual(result.logits.shape, (self.batch_size, self.num_labels)) + + def create_and_check_for_token_classification( + self, config, input_ids, input_mask, sequence_labels, token_labels + ): + config.num_labels = self.num_labels + model = ESMCForTokenClassification(config=config) + model.to(torch_device) + model.eval() + result = model(input_ids, attention_mask=input_mask, labels=token_labels) + self.parent.assertEqual(result.logits.shape, (self.batch_size, self.seq_length, self.num_labels)) + + def prepare_config_and_inputs_for_common(self): + config, input_ids, input_mask, sequence_labels, token_labels = self.prepare_config_and_inputs() + inputs_dict = {"input_ids": input_ids, "attention_mask": input_mask} + return config, inputs_dict + + +@require_torch +class ESMCModelTest(ModelTesterMixin, PipelineTesterMixin, unittest.TestCase): + test_mismatched_shapes = False + test_resize_embeddings = False # ESMC's lm_head is an nn.Sequential, not a tied decoder + + all_model_classes = ( + ( + ESMCModel, + ESMCForMaskedLM, + ESMCForSequenceClassification, + ESMCForTokenClassification, + ) + if is_torch_available() + else () + ) + pipeline_model_mapping = ( + { + "feature-extraction": ESMCModel, + "fill-mask": ESMCForMaskedLM, + "text-classification": ESMCForSequenceClassification, + "token-classification": ESMCForTokenClassification, + } + if is_torch_available() + else {} + ) + test_sequence_classification_problem_types = True + + def setUp(self): + self.model_tester = ESMCModelTester(self) + self.config_tester = ConfigTester(self, config_class=ESMCConfig, common_properties=["d_model", "n_heads"]) + + def test_config(self): + self.config_tester.run_common_tests() + + def test_model(self): + config_and_inputs = self.model_tester.prepare_config_and_inputs() + self.model_tester.create_and_check_model(*config_and_inputs) + + def test_for_masked_lm(self): + config_and_inputs = self.model_tester.prepare_config_and_inputs() + self.model_tester.create_and_check_for_masked_lm(*config_and_inputs) + + def test_for_sequence_classification(self): + config_and_inputs = self.model_tester.prepare_config_and_inputs() + self.model_tester.create_and_check_for_sequence_classification(*config_and_inputs) + + def test_for_token_classification(self): + config_and_inputs = self.model_tester.prepare_config_and_inputs() + self.model_tester.create_and_check_for_token_classification(*config_and_inputs) + + @unittest.skip( + reason="ESMC returns `hidden_states` as a single stacked tensor (used by the SAE feature), " + "not the live per-layer tensors, so grad does not flow back to the returned copy." + ) + def test_retain_grad_hidden_states_attentions(self): + pass + + +@slow +@require_torch +class ESMCModelIntegrationTest(unittest.TestCase): + def test_inference_masked_lm(self): + model = ESMCForMaskedLM.from_pretrained("biohub/ESMC-300M").to(torch_device).eval() + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") + inputs = tokenizer(["MKTAYIAKQR"], return_tensors="pt").to(torch_device) + with torch.no_grad(): + logits = model(**inputs).logits + self.assertEqual(logits.shape, (1, inputs["input_ids"].shape[1], model.config.vocab_size)) + self.assertTrue(torch.isfinite(logits).all()) From 6bd3270751edcbbf68847a9c3077ab74849f4760 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 15:07:22 +0100 Subject: [PATCH 12/70] Add ESMC docs + resolve auto_docstring checkpoint for the classification heads Add docs/source/en/model_doc/esmc.md (short: overview, links to the biohub/ESMC-{300M,600M,6B} checkpoints and the evolutionaryscale/esm repo, a usage snippet, and [[autodoc]] for ESMCConfig, ESMCSAEConfig, ESMCTokenizer, ESMCModel, ESMCFor{MaskedLM,SequenceClassification,TokenClassification}, ESMCSAEModel) and register it in _toctree.yml (alphabetically after ESM). ESMCForSequenceClassification/ForTokenClassification have no Examples block in their forward docstrings, so auto_docstring could not find a checkpoint and errored on import. Add the standard checkpoint sentence with a Hub link ([Biohub/ESMC-600M-2024-12](...)) to the ESMCConfig docstring; auto_docstring falls back to the config's checkpoint, resolving it for all model classes at once (no per-class decorator needed). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/source/en/_toctree.yml | 2 + docs/source/en/model_doc/esmc.md | 85 +++++++++++++++++++ .../models/esmc/configuration_esmc.py | 2 + 3 files changed, 89 insertions(+) create mode 100644 docs/source/en/model_doc/esmc.md diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index fe77a3d40ddf..e52623830af2 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -601,6 +601,8 @@ title: Ernie4_5_MoE - local: model_doc/esm title: ESM + - local: model_doc/esmc + title: ESMC - local: model_doc/eurobert title: EuroBERT - local: model_doc/exaone4 diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md new file mode 100644 index 000000000000..666b39867f71 --- /dev/null +++ b/docs/source/en/model_doc/esmc.md @@ -0,0 +1,85 @@ + + +# ESMC + +## Overview + +ESMC (ESM Cambrian) is a family of protein language models released by [EvolutionaryScale](https://www.evolutionaryscale.ai/). +It is a bidirectional Transformer encoder trained with a masked-language-modelling objective over amino-acid sequences, +using rotary position embeddings (RoPE), query/key LayerNorm and a SwiGLU feed-forward network. Like [ESM-2](./esm), +ESMC produces per-residue representations that are useful for downstream protein modelling tasks; unlike ESMFold it is a +sequence model only and does not predict structure. + +Pre-trained checkpoints are available on the Hugging Face Hub, including +[`biohub/ESMC-300M`](https://huggingface.co/biohub/ESMC-300M), +[`biohub/ESMC-600M`](https://huggingface.co/biohub/ESMC-600M) and +[`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B). The original code is available in the +[`evolutionaryscale/esm`](https://github.com/evolutionaryscale/esm) repository. + +## Usage example + +```python +import torch +from transformers import AutoTokenizer, ESMCModel + +tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") +model = ESMCModel.from_pretrained("biohub/ESMC-300M") + +inputs = tokenizer("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ", return_tensors="pt") +with torch.no_grad(): + outputs = model(**inputs) + +# Per-residue representations of shape (batch, sequence_length, d_model). +representations = outputs.last_hidden_state +``` + +## ESMCConfig + +[[autodoc]] ESMCConfig + +## ESMCSAEConfig + +[[autodoc]] ESMCSAEConfig + +## ESMCTokenizer + +[[autodoc]] ESMCTokenizer + +## ESMCModel + +[[autodoc]] ESMCModel + - forward + +## ESMCForMaskedLM + +[[autodoc]] ESMCForMaskedLM + - forward + +## ESMCForSequenceClassification + +[[autodoc]] ESMCForSequenceClassification + - forward + +## ESMCForTokenClassification + +[[autodoc]] ESMCForTokenClassification + - forward + +## ESMCSAEModel + +[[autodoc]] ESMCSAEModel + - forward diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py index 8d97ff9860f1..070c93f4cf7d 100644 --- a/src/transformers/models/esmc/configuration_esmc.py +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -20,6 +20,8 @@ class ESMCConfig(PretrainedConfig): """ This is the configuration class to store the configuration of a [`ESMCModel`]. It is used to instantiate an ESMC model according to the specified arguments, defining the model architecture. + Instantiating a configuration with the defaults will yield a similar configuration to that of the + [Biohub/ESMC-600M-2024-12](https://huggingface.co/Biohub/ESMC-600M-2024-12) architecture. Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the documentation from [`PretrainedConfig`] for more information. From 52c951942e8753db2d0b3d20cbac902755d29501 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 15:31:46 +0100 Subject: [PATCH 13/70] =?UTF-8?q?Remove=20the=20SAE=20(esmc=5Fsae)=20from?= =?UTF-8?q?=20ESMC=20=E2=80=94=20deferred=20to=20a=20follow-up=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sparse-autoencoder is architecturally distinct from ESMC (a linear encoder + top-k sparsity + linear decoder operating on frozen hidden states, not a transformer) and has a non-standard per-layer from_pretrained/ save_pretrained path. Keep it out of the core ESMC PR; it will land as its own follow-up (recoverable from this commit's parent). Removed: - modeling_esmc_sae.py and configuration_esmc_sae.py. - esmc_sae auto-registration (CONFIG_MAPPING_NAMES, MODEL_MAPPING_NAMES, and the esmc_sae -> esmc SPECIAL_MODEL_TYPE_TO_MODULE_NAME mapping). - The SAE integration woven through ESMCModel and the heads: add_sae_models, _get_sae_outputs / _get_sae_layer_num_requested / _validate_sae_inputs / _SAE_KEY_RE / _sae_models, the compute_sae/normalize_sae params, and the sae_outputs field on all four output dataclasses. The forward simplifies accordingly (layers_to_collect now only serves output_hidden_states; the SAE-only bool_mask is gone). - ESMCSAEConfig/ESMCSAEModel autodoc from the model doc. Source of truth is modular_esmc.py; modeling_esmc.py regenerated. Verified: ESMCSAE* no longer importable and esmc_sae deregistered; ruff clean; model + tokenizer tests pass (102 passed, 103 skipped); and fork-vs-new logits on biohub/ESMC-300M remain bit-identical (max|Δ| = 0.0) — the removal is output-neutral for the core model. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/source/en/model_doc/esmc.md | 9 - src/transformers/models/auto/auto_mappings.py | 2 - src/transformers/models/auto/modeling_auto.py | 1 - src/transformers/models/esmc/__init__.py | 3 +- .../models/esmc/configuration_esmc_sae.py | 77 ---- src/transformers/models/esmc/modeling_esmc.py | 189 +-------- .../models/esmc/modeling_esmc_sae.py | 363 ------------------ src/transformers/models/esmc/modular_esmc.py | 189 +-------- 8 files changed, 9 insertions(+), 824 deletions(-) delete mode 100644 src/transformers/models/esmc/configuration_esmc_sae.py delete mode 100644 src/transformers/models/esmc/modeling_esmc_sae.py diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 666b39867f71..1d3c3de42826 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -51,10 +51,6 @@ representations = outputs.last_hidden_state [[autodoc]] ESMCConfig -## ESMCSAEConfig - -[[autodoc]] ESMCSAEConfig - ## ESMCTokenizer [[autodoc]] ESMCTokenizer @@ -78,8 +74,3 @@ representations = outputs.last_hidden_state [[autodoc]] ESMCForTokenClassification - forward - -## ESMCSAEModel - -[[autodoc]] ESMCSAEModel - - forward diff --git a/src/transformers/models/auto/auto_mappings.py b/src/transformers/models/auto/auto_mappings.py index 905ebb97a884..a850f926a5cd 100644 --- a/src/transformers/models/auto/auto_mappings.py +++ b/src/transformers/models/auto/auto_mappings.py @@ -177,7 +177,6 @@ ("ernie4_5_vl_moe_vision", "Ernie4_5_VLMoeVisionConfig"), ("esm", "EsmConfig"), ("esmc", "ESMCConfig"), - ("esmc_sae", "ESMCSAEConfig"), ("esmfold2", "ESMFold2Config"), ("eurobert", "EuroBertConfig"), ("evolla", "EvollaConfig"), @@ -744,7 +743,6 @@ ("encoder-decoder", "encoder_decoder"), ("ernie4_5_vl_moe_text", "ernie4_5_vl_moe"), ("ernie4_5_vl_moe_vision", "ernie4_5_vl_moe"), - ("esmc_sae", "esmc"), ("esmfold2_v2", "esmfold2"), ("exaone4_5_vision", "exaone4_5"), ("fastspeech2_conformer_hifigan", "fastspeech2_conformer"), diff --git a/src/transformers/models/auto/modeling_auto.py b/src/transformers/models/auto/modeling_auto.py index 625351c1bd85..42cc5ab2dd97 100644 --- a/src/transformers/models/auto/modeling_auto.py +++ b/src/transformers/models/auto/modeling_auto.py @@ -153,7 +153,6 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin): ("ernie4_5_vl_moe", "Ernie4_5_VLMoeModel"), ("esm", "EsmModel"), ("esmc", "ESMCModel"), - ("esmc_sae", "ESMCSAEModel"), ("esmfold2", "ESMFold2Model"), ("eurobert", "EuroBertModel"), ("evolla", "EvollaModel"), diff --git a/src/transformers/models/esmc/__init__.py b/src/transformers/models/esmc/__init__.py index 07d913a15255..750ed540a2c7 100644 --- a/src/transformers/models/esmc/__init__.py +++ b/src/transformers/models/esmc/__init__.py @@ -16,11 +16,10 @@ from ...utils import _LazyModule # type: ignore[import] from ...utils.import_utils import define_import_structure # type: ignore[import] + if TYPE_CHECKING: from .configuration_esmc import * # noqa: F403 - from .configuration_esmc_sae import * # noqa: F403 from .modeling_esmc import * # noqa: F403 - from .modeling_esmc_sae import * # noqa: F403 from .tokenization_esmc import * # noqa: F403 else: import sys diff --git a/src/transformers/models/esmc/configuration_esmc_sae.py b/src/transformers/models/esmc/configuration_esmc_sae.py deleted file mode 100644 index 42ecc1f32c09..000000000000 --- a/src/transformers/models/esmc/configuration_esmc_sae.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2026 Biohub. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""ESMC sparse autoencoder (SAE) configuration.""" - -from dataclasses import dataclass - -from ...configuration_utils import PretrainedConfig # type: ignore[import] - - -@dataclass -class ESMCSAEParams: - """Parameters for one backbone layer's SAE inside :class:`ESMCSAEModel`. - - The SAE itself is an internal ``nn.Module``; this dataclass just bundles - the handful of fields needed to instantiate one. - """ - - d_model: int = 2560 - codebook_dim: int = 65536 - k: int = 64 - layer: int = 0 - - -class ESMCSAEConfig(PretrainedConfig): - """ - Configuration class for [`ESMCSAEModel`] — a container that holds one - SAE per backbone layer for a fixed ``(model, codebook_dim, k)`` group. - - All SAEs in a container share ``d_model``, ``codebook_dim``, and ``k``; - they differ only in the backbone layer they were trained on. - ``available_layers`` lists the backbone-layer indices the repo ships; - each entry ``i`` is stored on disk as ``layer_{i}.safetensors`` (the - filename index *is* the backbone layer, so a single-layer repo for - layer 23 stores ``layer_23.safetensors``). - - Args: - d_model (`int`, *optional*, defaults to 2560): - Dimensionality of the ESMC hidden states fed into the SAEs. - codebook_dim (`int`, *optional*, defaults to 65536): - Number of sparse features in each SAE's codebook. - k (`int`, *optional*, defaults to 64): - Top-k sparsity per SAE. - available_layers (`list[int]`, *optional*, defaults to ``[0]``): - Which backbone-layer indices the repo ships. - """ - - model_type = "esmc_sae" - - def __init__( - self, - d_model: int = 2560, - codebook_dim: int = 65536, - k: int = 64, - available_layers: list[int] | None = None, - **kwargs, - ): - super().__init__(**kwargs) - self.d_model = d_model - self.codebook_dim = codebook_dim - self.k = k - self.available_layers = ( - list(available_layers) if available_layers is not None else [0] - ) - - -__all__ = ["ESMCSAEConfig", "ESMCSAEParams"] diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 38f55f19071b..a1ffd1b62858 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -19,10 +19,8 @@ # limitations under the License. import math -import re from collections.abc import Callable from dataclasses import dataclass -from typing import cast import torch import torch.nn as nn @@ -42,7 +40,6 @@ from ...processing_utils import Unpack from ...utils import TransformersKwargs, auto_docstring, can_return_tuple # type: ignore[import] from .configuration_esmc import ESMCConfig -from .modeling_esmc_sae import _ESMCSAELayer # --------------------------------------------------------------------------- @@ -60,10 +57,6 @@ class ESMCOutput(ModelOutput): Stacked hidden states for all encoder layers. Shape ``(n_layers, batch_size, sequence_length, d_model)``. Returned when ``output_hidden_states=True``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). - Only populated when SAE models have been registered via - ``add_sae_models`` and ``compute_sae=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -73,7 +66,6 @@ class ESMCOutput(ModelOutput): last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -89,8 +81,6 @@ class ESMCMaskedLMOutput(MaskedLMOutput): Final hidden states after layer normalisation. hidden_states (`torch.FloatTensor`, *optional*): Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -101,7 +91,6 @@ class ESMCMaskedLMOutput(MaskedLMOutput): logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -117,8 +106,6 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): Final hidden states after layer normalisation. hidden_states (`torch.FloatTensor`, *optional*): Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -129,7 +116,6 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -145,8 +131,6 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): Final hidden states after layer normalisation. hidden_states (`torch.FloatTensor`, *optional*): Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -157,7 +141,6 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -710,7 +693,6 @@ def __init__(self, config: ESMCConfig): config.n_heads, config.n_layers, ) - self._sae_models: nn.ModuleDict = nn.ModuleDict() self.post_init() def get_input_embeddings(self) -> nn.Embedding: @@ -719,94 +701,6 @@ def get_input_embeddings(self) -> nn.Embedding: def set_input_embeddings(self, value: nn.Embedding): self.embed = value - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Register one or more SAEs obtained from an :class:`ESMCSAEModel`. - - Each is keyed by ``f"layer{N}"`` (the backbone-layer index ``N`` the - SAE is trained against, set by - :meth:`ESMCSAEModel.initialize_layers`). Attaching two SAEs for the - same backbone layer raises — only one SAE per layer can be active. - - Example:: - - sae = ESMCSAEModel.from_pretrained( - "biohub/esmc-600m-2024-12-sae-k64-codebook16384" - ) - sae.initialize_layers([27, 33]) - model.add_sae_models([sae.layers["27"], sae.layers["33"]]) - """ - for layer in sae_models: - assert isinstance(layer, _ESMCSAELayer), ( - f"Expected an SAE layer (model.layers['']), got {type(layer).__name__}." - ) - key = f"layer{int(layer.layer)}" - if key in self._sae_models: - raise ValueError( - f"An SAE is already registered at {key!r}. Only one SAE " - "per backbone layer can be active — pick a different " - "layer on one of them, or attach in a fresh model." - ) - self._sae_models[key] = layer - - _SAE_KEY_RE = re.compile(r"layer(\d+)") - - def _get_sae_layer_num_requested(self, model_name: str) -> int: - """Recover the backbone-layer index from a key written by - :meth:`add_sae_models` (``"layer{N}"`` → ``N``).""" - match = self._SAE_KEY_RE.fullmatch(model_name) - assert match is not None, f"Unexpected SAE key {model_name!r}; expected 'layer{{N}}'." - return int(match.group(1)) - - def _validate_sae_inputs(self, input_ids: torch.Tensor) -> None: - assert torch.all(input_ids != self.config.mask_token_id), ( - "SAE inputs must not contain mask tokens. SAEs were trained on unmasked sequences." - ) - - def _get_sae_outputs( - self, - hidden_states: torch.Tensor, - layers_to_collect: list[int], - token_mask: torch.Tensor, - normalize_sae: bool = False, - ) -> dict[str, torch.Tensor]: - """Run all registered SAEs and return their feature magnitudes. - - Args: - hidden_states: Stacked tensor of shape - ``(len(layers_to_collect), batch, seq_len, d_model)``. - layers_to_collect: The ESMC layer indices that were collected, - in the same order as the first dim of ``hidden_states``. - token_mask: Boolean mask ``(batch, seq_len)`` — ``True`` for - real (non-padding) tokens. - normalize_sae: When ``True``, scale features by ``idf / max`` - using the per-feature stats trained alongside each SAE. - """ - layer_to_idx = {layer: idx for idx, layer in enumerate(layers_to_collect)} - sae_outputs: dict[str, torch.Tensor] = {} - - for model_name, sae_module in self._sae_models.items(): - # `nn.ModuleDict` only stores `nn.Module`s at the type level; - # ``add_sae_models`` enforces that each entry is an ``_ESMCSAELayer``. - assert isinstance(sae_module, _ESMCSAELayer) - layer: _ESMCSAELayer = sae_module - requested_layer = self._get_sae_layer_num_requested(model_name) - layer_idx = layer_to_idx[requested_layer] - layer_states = hidden_states[layer_idx].clone().to(self.device) - - sae_out = layer.get_sae_output(layer_states, token_mask) - features = sae_out.feature_magnitudes.detach() - - if normalize_sae: - # ``register_buffer`` is typed as ``Tensor | Module`` on - # ``nn.Module``; narrow here since these are Tensors. - idf = cast(torch.Tensor, layer.idf) - max_val = cast(torch.Tensor, layer.max) - features = (features / max_val) * idf - - sae_outputs[model_name] = features.to_sparse() - - return sae_outputs - @can_return_tuple @auto_docstring def forward( @@ -817,8 +711,6 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -834,13 +726,6 @@ def forward( Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the attention probabilities are observable; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run any SAE models registered via :meth:`add_sae_models`. - Has no effect when no SAEs are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE feature magnitudes by ``idf / max`` (only - applied when the SAE's normalization buffers contain non-trivial values). - Examples: ```python @@ -860,26 +745,12 @@ def forward( output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions return_dict = return_dict if return_dict is not None else self.config.use_return_dict - output_sae = compute_sae and len(self._sae_models) > 0 - - # Determine which intermediate layers to collect. When SAEs are - # registered we must collect at least the layers they target, even if - # the caller did not ask for all hidden states. - if output_hidden_states: - layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) - elif output_sae: - layers_to_collect = sorted({self._get_sae_layer_num_requested(name) for name in self._sae_models}) - else: - layers_to_collect = [] + # Collect per-layer hidden states only when the caller asks for them. + layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) if output_hidden_states else [] user_supplied_sequence_id = sequence_id is not None - if user_supplied_sequence_id: - bool_mask = sequence_id >= 0 - else: - if attention_mask is None: - attention_mask = input_ids != self.config.pad_token_id - assert attention_mask is not None - bool_mask = attention_mask.bool() + if not user_supplied_sequence_id and attention_mask is None: + attention_mask = input_ids != self.config.pad_token_id x = self.embed(input_ids) position_ids = torch.arange(x.shape[1], device=x.device).unsqueeze(0) @@ -916,17 +787,9 @@ def forward( output_attentions=output_attentions, ) - # Stack once; reused for both SAE and hidden-state output. collected_tensor: torch.Tensor | None = ( torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] ) - - sae_outputs: dict[str, torch.Tensor] | None = None - if output_sae and collected_tensor is not None: - assert input_ids is not None - self._validate_sae_inputs(input_ids) - sae_outputs = self._get_sae_outputs(collected_tensor, layers_to_collect, bool_mask, normalize_sae) - hidden_states_tensor = collected_tensor if output_hidden_states else None if not return_dict: @@ -935,7 +798,6 @@ def forward( for v in [ last_hidden_state, hidden_states_tensor, - sae_outputs, attentions, ] if v is not None @@ -944,7 +806,6 @@ def forward( return ESMCOutput( last_hidden_state=last_hidden_state, hidden_states=hidden_states_tensor, - sae_outputs=sae_outputs, attentions=attentions, ) @@ -991,10 +852,6 @@ def get_output_embeddings(self) -> nn.Linear: def set_output_embeddings(self, new_embeddings: nn.Linear): self.lm_head[-1] = new_embeddings - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Proxy to :meth:`ESMCModel.add_sae_models`.""" - self.esmc.add_sae_models(sae_models) - @can_return_tuple @auto_docstring def forward( @@ -1006,8 +863,6 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -1019,10 +874,6 @@ def forward( output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run registered SAE models. Has no effect when none are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE features by ``idf / max`` normalization buffers. Examples: @@ -1047,8 +898,6 @@ def forward( output_hidden_states=output_hidden_states, output_attentions=output_attentions, return_dict=True, - compute_sae=compute_sae, - normalize_sae=normalize_sae, ) logits = self.lm_head(encoder_outputs.last_hidden_state) @@ -1065,7 +914,6 @@ def forward( logits, encoder_outputs.last_hidden_state, encoder_outputs.hidden_states, - encoder_outputs.sae_outputs, encoder_outputs.attentions, ] if v is not None @@ -1076,7 +924,6 @@ def forward( logits=logits, last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, - sae_outputs=encoder_outputs.sae_outputs, attentions=encoder_outputs.attentions, ) @@ -1124,10 +971,6 @@ def __init__(self, config: ESMCConfig): self.classifier = _ESMCClassificationHead(config) self.post_init() - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Proxy to :meth:`ESMCModel.add_sae_models`.""" - self.esmc.add_sae_models(sae_models) - @can_return_tuple @auto_docstring def forward( @@ -1138,8 +981,6 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: r""" labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): @@ -1149,10 +990,6 @@ def forward( output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run registered SAE models. Has no effect when none are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE features by ``idf / max`` normalization buffers. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict @@ -1162,8 +999,6 @@ def forward( output_hidden_states=output_hidden_states, output_attentions=output_attentions, return_dict=True, - compute_sae=compute_sae, - normalize_sae=normalize_sae, ) logits = self.classifier(encoder_outputs.last_hidden_state) @@ -1198,7 +1033,6 @@ def forward( logits, encoder_outputs.last_hidden_state, encoder_outputs.hidden_states, - encoder_outputs.sae_outputs, encoder_outputs.attentions, ] if v is not None @@ -1209,7 +1043,6 @@ def forward( logits=logits, last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, - sae_outputs=encoder_outputs.sae_outputs, attentions=encoder_outputs.attentions, ) @@ -1235,10 +1068,6 @@ def __init__(self, config: ESMCConfig): self.classifier = nn.Linear(config.d_model, config.num_labels) self.post_init() - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Proxy to :meth:`ESMCModel.add_sae_models`.""" - self.esmc.add_sae_models(sae_models) - @can_return_tuple @auto_docstring def forward( @@ -1249,8 +1078,6 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: r""" labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -1259,10 +1086,6 @@ def forward( output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run registered SAE models. Has no effect when none are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE features by ``idf / max`` normalization buffers. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict @@ -1272,8 +1095,6 @@ def forward( output_hidden_states=output_hidden_states, output_attentions=output_attentions, return_dict=True, - compute_sae=compute_sae, - normalize_sae=normalize_sae, ) sequence_output = self.dropout(encoder_outputs.last_hidden_state) @@ -1293,7 +1114,6 @@ def forward( logits, encoder_outputs.last_hidden_state, encoder_outputs.hidden_states, - encoder_outputs.sae_outputs, encoder_outputs.attentions, ] if v is not None @@ -1304,7 +1124,6 @@ def forward( logits=logits, last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, - sae_outputs=encoder_outputs.sae_outputs, attentions=encoder_outputs.attentions, ) diff --git a/src/transformers/models/esmc/modeling_esmc_sae.py b/src/transformers/models/esmc/modeling_esmc_sae.py deleted file mode 100644 index b706e50e9153..000000000000 --- a/src/transformers/models/esmc/modeling_esmc_sae.py +++ /dev/null @@ -1,363 +0,0 @@ -# Copyright 2026 Biohub. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""PyTorch ESMC SAE (Sparse Autoencoder) model. - -* :class:`ESMCSAEModel` — the published HF container, one repo per - ``(backbone, codebook_dim, k)`` group. Each backbone layer ships as a - ``layer_{i}.safetensors`` shard; ``from_pretrained`` downloads the whole - snapshot but loads no weights — callers materialize the layers they need - via :meth:`initialize_layers`. Single-layer repos auto-load so bare - ``forward(x)`` works. -* :class:`_ESMCSAELayer` — internal ``nn.Module`` that holds the weights for - one ``(backbone, codebook_dim, k, layer)`` SAE. Not a published HF artifact; - obtained only via ``model.layers[""]``. -""" - -from __future__ import annotations - -import os -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -import torch -import torch.nn as nn -import torch.nn.functional as F -from safetensors.torch import load_file, save_file - -from ...modeling_outputs import ModelOutput # type: ignore[import] -from ...modeling_utils import PreTrainedModel # type: ignore[import] -from ...utils import auto_docstring # type: ignore[import] -from .configuration_esmc_sae import ESMCSAEConfig, ESMCSAEParams - - -@dataclass -@auto_docstring( - custom_intro=""" - Output type of [`ESMCSAEModel`]. - """ -) -class ESMCSAEOutput(ModelOutput): - feature_magnitudes: torch.Tensor - reconstruction_loss: Optional[torch.Tensor] = None - - def to_sparse(self) -> None: - self.feature_magnitudes = self.feature_magnitudes.to_sparse() - - -class _ESMCSAELayer(nn.Module): - """One backbone layer's SAE — internal building block of :class:`ESMCSAEModel`. - - Not exposed via ``AutoModel`` and not loadable on its own. Obtain one - via ``model.layers[""]`` after calling ``initialize_layers``. - """ - - def __init__(self, params: ESMCSAEParams): - super().__init__() - self.params = params - - self.W_enc = nn.Parameter(torch.empty(params.d_model, params.codebook_dim)) - self.W_dec = nn.Parameter(torch.empty(params.codebook_dim, params.d_model)) - self.b_dec = nn.Parameter(torch.zeros(params.d_model)) - # Per-feature normalization stats. Trained alongside the SAE for some - # variants; for variants that don't ship them, leaving these as ones - # makes ``_get_sae_outputs``'s ``features / max * idf`` a no-op. - self.register_buffer("idf", torch.ones(params.codebook_dim)) - self.register_buffer("max", torch.ones(params.codebook_dim)) - - @property - def layer(self) -> int: - """Backbone-layer index this SAE is trained against.""" - return self.params.layer - - def forward(self, x: torch.Tensor, **_kwargs: object) -> ESMCSAEOutput: - del _kwargs - x = self._zscore_normalize_representation(x) - - x_with_pre_encoder_bias = x - self.b_dec - preactivations = F.relu(x_with_pre_encoder_bias @ self.W_enc) - - topk = torch.topk(preactivations, self.params.k, dim=-1) - feature_magnitudes = torch.zeros_like(preactivations).scatter( - -1, topk.indices, topk.values - ) - - reconstructed = feature_magnitudes @ self.W_dec + self.b_dec - - reconstruction_loss = (reconstructed - x).pow(2).mean(dim=-1) - - return ESMCSAEOutput( - feature_magnitudes=feature_magnitudes, - reconstruction_loss=reconstruction_loss, - ) - - def get_sae_output( - self, layer_states: torch.Tensor, token_mask: torch.Tensor - ) -> ESMCSAEOutput: - _, _, v_len = layer_states.shape - nonpad_states = layer_states[token_mask].view(-1, v_len) - return self(nonpad_states) - - def _zscore_normalize_representation(self, x: torch.Tensor) -> torch.Tensor: - x_mean = x.mean(dim=-1, keepdim=True) - x = x - x_mean - x_std = x.std(dim=-1, keepdim=True) - return x / (x_std + 1e-5) - - -@auto_docstring -class ESMCSAEPreTrainedModel(PreTrainedModel): - config_class = ESMCSAEConfig - base_model_prefix = "esmc_sae" - - -@auto_docstring( - custom_intro=""" - HF container holding one SAE per backbone layer, all sharing the same - ``(d_model, codebook_dim, k)``. - - ``from_pretrained`` downloads the entire repo (every ``layer_{i}.safetensors``) - into the local HF cache but does **not** load any weights into memory. - Callers materialize the layers they actually need by calling - :meth:`initialize_layers`. The full set is available on disk after the - first call, so subsequent layer switches read from the local cache without - re-downloading. - - Examples:: - - model = ESMCSAEModel.from_pretrained( - "biohub/esmc-6b-2024-12-sae-k64-codebook16384" - ) - model.initialize_layers([60]) # ~2.5 GB into memory - out = model(layer_states, layer=60) # forward through layer 60 - model.initialize_layers([45]) # add layer 45 (cached locally) - model.release_layer(60) # free layer 60 - """ -) -class ESMCSAEModel(ESMCSAEPreTrainedModel): - def __init__(self, config: ESMCSAEConfig): - super().__init__(config) - # Layers are populated lazily by ``initialize_layers``; the container - # starts empty so ``from_pretrained`` doesn't materialize hundreds of - # GB of unused parameters. - self.layers = nn.ModuleDict() - # Zero-element buffer that rides along with ``.to(device/dtype)``. - # ``initialize_layers`` reads its current device/dtype so SAEs added - # after ``model.to("cuda")`` land on CUDA without re-passing ``device=``. - self.register_buffer("_device_marker", torch.empty(0), persistent=False) - self._snapshot_dir: Optional[str] = None - self.post_init() - - @classmethod - def from_pretrained( # type: ignore[override] - cls, pretrained_model_name_or_path: str | os.PathLike, *model_args, **kwargs - ) -> "ESMCSAEModel": - """Download (or reuse cached) the full repo and return the model. - - By default no weights are read into memory and the caller must invoke - :meth:`initialize_layers` before running :meth:`forward`. The single - exception is when the repo ships exactly one layer: that layer is - auto-loaded (honoring ``torch_dtype`` / ``device`` if passed) so the - bare ``forward(x)`` call just works. - - Honored kwargs: ``revision``, ``cache_dir``, ``token``, - ``allow_patterns``, ``local_files_only``, ``force_download`` (forwarded - to ``snapshot_download``); ``torch_dtype`` and ``device`` (used by the - single-layer auto-load path; otherwise pass them to - :meth:`initialize_layers`). Behavioral kwargs that imply work we do - not perform (``device_map``, ``low_cpu_mem_usage``, - ``quantization_config``, ``attn_implementation``) raise so the user - isn't silently misled. Other HF housekeeping kwargs (``config``, - ``trust_remote_code``, ``adapter_kwargs``, …) are accepted and - ignored — they only matter for the standard loader, which we bypass. - """ - del model_args - torch_dtype = kwargs.pop("torch_dtype", None) - device = kwargs.pop("device", None) - local_dir = _resolve_snapshot_dir(pretrained_model_name_or_path, kwargs) - unsupported = { - "device_map", - "low_cpu_mem_usage", - "quantization_config", - "attn_implementation", - "max_memory", - "offload_folder", - "offload_state_dict", - } & kwargs.keys() - if unsupported: - raise TypeError( - f"Unsupported kwargs to ESMCSAEModel.from_pretrained: " - f"{sorted(unsupported)}. The standard HF loader is bypassed —" - " call initialize_layers(..., device=, dtype=) instead." - ) - config = ESMCSAEConfig.from_pretrained(local_dir) - model = cls(config) - model._snapshot_dir = str(local_dir) - if device is not None: - model.to(device) - if torch_dtype is not None: - model.to(torch_dtype) - if len(config.available_layers) == 1: - model.initialize_layers(list(config.available_layers)) - return model - - def initialize_layers( - self, - layers: list[int], - *, - device: torch.device | str | None = None, - dtype: torch.dtype | None = None, - ) -> None: - """Load the requested layers from the local snapshot into memory. - - Layers already present in :attr:`self.layers` are skipped — calling - ``initialize_layers([23])`` twice is idempotent. ``device`` / ``dtype`` - default to wherever the model itself lives (via the ``_device_marker`` - buffer that moves with ``.to(...)``), so the common pattern of - ``model.to("cuda"); model.initialize_layers([7])`` Just Works. - """ - assert self._snapshot_dir is not None, ( - "ESMCSAEModel has no snapshot directory — call " - "from_pretrained first, or set _snapshot_dir manually." - ) - if device is None: - device = self._device_marker.device - if dtype is None: - dtype = self._device_marker.dtype - snapshot_dir = Path(self._snapshot_dir) - available = set(self.config.available_layers) - for layer_idx in layers: - key = str(layer_idx) - if key in self.layers: - continue - if layer_idx not in available: - raise KeyError( - f"Layer {layer_idx} is not in this repo. " - f"available_layers={sorted(available)}" - ) - shard = snapshot_dir / f"layer_{layer_idx}.safetensors" - if not shard.exists(): - raise FileNotFoundError( - f"Missing layer file {shard} — config lists layer " - f"{layer_idx} as available but the shard is not on disk." - ) - params = ESMCSAEParams( - d_model=self.config.d_model, - codebook_dim=self.config.codebook_dim, - k=self.config.k, - layer=layer_idx, - ) - # Build on the meta device so we don't allocate weights that - # ``load_state_dict`` would immediately overwrite. - with torch.device("meta"): - layer = _ESMCSAELayer(params) - layer.to_empty(device=device) - layer.load_state_dict(load_file(str(shard))) - layer.to(dtype=dtype) - self.layers[key] = layer - - def release_layer(self, layer: int) -> None: - """Drop the named layer from memory. No-op if not loaded.""" - key = str(layer) - if key in self.layers: - del self.layers[key] - - def loaded_layers(self) -> list[int]: - """Sorted list of layer indices currently materialized in memory.""" - return sorted(int(k) for k in self.layers.keys()) - - def forward( - self, x: torch.Tensor, layer: int | None = None, **kwargs: object - ) -> ESMCSAEOutput: - if layer is None: - if len(self.layers) == 1: - # Unambiguous: exactly one layer loaded → use it. - ((_only_key, only_layer),) = self.layers.items() - return only_layer(x, **kwargs) - if len(self.layers) == 0: - raise RuntimeError( - "No layers loaded — call " - f"initialize_layers([...]) first. " - f"available_layers={self.config.available_layers}" - ) - raise RuntimeError( - "Multiple layers are loaded — please select one via " - f"forward(x, layer=). Loaded layers: {self.loaded_layers()}" - ) - key = str(layer) - if key not in self.layers: - raise KeyError( - f"Layer {layer} is not loaded. Call " - f"initialize_layers([{layer}]) first. Loaded layers: " - f"{self.loaded_layers()}" - ) - return self.layers[key](x, **kwargs) - - def save_pretrained( # type: ignore[override] - self, save_directory: str | os.PathLike, *args, **kwargs - ) -> None: - """Write ``config.json`` plus one ``layer_{i}.safetensors`` per loaded layer. - - Only layers currently in :attr:`self.layers` are written. - ``available_layers`` in the saved config is synced to what's actually - on disk so a ``release_layer`` + ``save_pretrained`` round-trip never - advertises a layer whose shard is missing. - """ - del args, kwargs - save_directory = Path(save_directory) - save_directory.mkdir(parents=True, exist_ok=True) - # Sync available_layers to what we're about to write — never advertise - # a layer that isn't on disk in this repo. - self.config.available_layers = self.loaded_layers() - self.config.save_pretrained(str(save_directory)) - for key, layer in self.layers.items(): - shard = save_directory / f"layer_{key}.safetensors" - save_file( - { - k: v.detach().cpu().contiguous() - for k, v in layer.state_dict().items() - }, - str(shard), - ) - - -def _resolve_snapshot_dir( - pretrained_model_name_or_path: str | os.PathLike, kwargs: dict -) -> str: - """Local dir → return as-is; hub id → ``snapshot_download`` it. - - A directory only counts as "local" if it actually contains ``config.json``, - so a stale subdir named like a hub id (``./biohub/esmc-...``) - doesn't accidentally shadow the hub fetch. - - Pops the standard ``snapshot_download`` keyword args from ``kwargs`` so - callers can forward them via ``from_pretrained``. - """ - path = Path(pretrained_model_name_or_path) - if path.is_dir() and (path / "config.json").exists(): - return str(path) - from huggingface_hub import snapshot_download - - return snapshot_download( - repo_id=str(pretrained_model_name_or_path), - revision=kwargs.pop("revision", None), - cache_dir=kwargs.pop("cache_dir", None), - token=kwargs.pop("token", None), - allow_patterns=kwargs.pop("allow_patterns", None), - local_files_only=kwargs.pop("local_files_only", False), - force_download=kwargs.pop("force_download", False), - ) - - -__all__ = ["ESMCSAEModel", "ESMCSAEOutput", "ESMCSAEPreTrainedModel"] diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 5560f1eacdce..2440dd0412f2 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -14,10 +14,8 @@ """PyTorch ESMC model.""" import math -import re from collections.abc import Callable from dataclasses import dataclass -from typing import cast import torch import torch.nn as nn @@ -43,7 +41,6 @@ eager_attention_forward, ) from .configuration_esmc import ESMCConfig -from .modeling_esmc_sae import _ESMCSAELayer logger = logging.get_logger(__name__) @@ -65,10 +62,6 @@ class ESMCOutput(ModelOutput): Stacked hidden states for all encoder layers. Shape ``(n_layers, batch_size, sequence_length, d_model)``. Returned when ``output_hidden_states=True``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). - Only populated when SAE models have been registered via - ``add_sae_models`` and ``compute_sae=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -78,7 +71,6 @@ class ESMCOutput(ModelOutput): last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -94,8 +86,6 @@ class ESMCMaskedLMOutput(MaskedLMOutput): Final hidden states after layer normalisation. hidden_states (`torch.FloatTensor`, *optional*): Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -106,7 +96,6 @@ class ESMCMaskedLMOutput(MaskedLMOutput): logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -122,8 +111,6 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): Final hidden states after layer normalisation. hidden_states (`torch.FloatTensor`, *optional*): Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -134,7 +121,6 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -150,8 +136,6 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): Final hidden states after layer normalisation. hidden_states (`torch.FloatTensor`, *optional*): Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. - sae_outputs (`dict[str, torch.Tensor]`, *optional*): - SAE feature magnitudes keyed by SAE model name (sparse tensors). attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -162,7 +146,6 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None hidden_states: torch.FloatTensor | None = None - sae_outputs: dict[str, torch.Tensor] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -653,7 +636,6 @@ def __init__(self, config: ESMCConfig): config.n_heads, config.n_layers, ) - self._sae_models: nn.ModuleDict = nn.ModuleDict() self.post_init() def get_input_embeddings(self) -> nn.Embedding: @@ -662,94 +644,6 @@ def get_input_embeddings(self) -> nn.Embedding: def set_input_embeddings(self, value: nn.Embedding): self.embed = value - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Register one or more SAEs obtained from an :class:`ESMCSAEModel`. - - Each is keyed by ``f"layer{N}"`` (the backbone-layer index ``N`` the - SAE is trained against, set by - :meth:`ESMCSAEModel.initialize_layers`). Attaching two SAEs for the - same backbone layer raises — only one SAE per layer can be active. - - Example:: - - sae = ESMCSAEModel.from_pretrained( - "biohub/esmc-600m-2024-12-sae-k64-codebook16384" - ) - sae.initialize_layers([27, 33]) - model.add_sae_models([sae.layers["27"], sae.layers["33"]]) - """ - for layer in sae_models: - assert isinstance(layer, _ESMCSAELayer), ( - f"Expected an SAE layer (model.layers['']), got {type(layer).__name__}." - ) - key = f"layer{int(layer.layer)}" - if key in self._sae_models: - raise ValueError( - f"An SAE is already registered at {key!r}. Only one SAE " - "per backbone layer can be active — pick a different " - "layer on one of them, or attach in a fresh model." - ) - self._sae_models[key] = layer - - _SAE_KEY_RE = re.compile(r"layer(\d+)") - - def _get_sae_layer_num_requested(self, model_name: str) -> int: - """Recover the backbone-layer index from a key written by - :meth:`add_sae_models` (``"layer{N}"`` → ``N``).""" - match = self._SAE_KEY_RE.fullmatch(model_name) - assert match is not None, f"Unexpected SAE key {model_name!r}; expected 'layer{{N}}'." - return int(match.group(1)) - - def _validate_sae_inputs(self, input_ids: torch.Tensor) -> None: - assert torch.all(input_ids != self.config.mask_token_id), ( - "SAE inputs must not contain mask tokens. SAEs were trained on unmasked sequences." - ) - - def _get_sae_outputs( - self, - hidden_states: torch.Tensor, - layers_to_collect: list[int], - token_mask: torch.Tensor, - normalize_sae: bool = False, - ) -> dict[str, torch.Tensor]: - """Run all registered SAEs and return their feature magnitudes. - - Args: - hidden_states: Stacked tensor of shape - ``(len(layers_to_collect), batch, seq_len, d_model)``. - layers_to_collect: The ESMC layer indices that were collected, - in the same order as the first dim of ``hidden_states``. - token_mask: Boolean mask ``(batch, seq_len)`` — ``True`` for - real (non-padding) tokens. - normalize_sae: When ``True``, scale features by ``idf / max`` - using the per-feature stats trained alongside each SAE. - """ - layer_to_idx = {layer: idx for idx, layer in enumerate(layers_to_collect)} - sae_outputs: dict[str, torch.Tensor] = {} - - for model_name, sae_module in self._sae_models.items(): - # `nn.ModuleDict` only stores `nn.Module`s at the type level; - # ``add_sae_models`` enforces that each entry is an ``_ESMCSAELayer``. - assert isinstance(sae_module, _ESMCSAELayer) - layer: _ESMCSAELayer = sae_module - requested_layer = self._get_sae_layer_num_requested(model_name) - layer_idx = layer_to_idx[requested_layer] - layer_states = hidden_states[layer_idx].clone().to(self.device) - - sae_out = layer.get_sae_output(layer_states, token_mask) - features = sae_out.feature_magnitudes.detach() - - if normalize_sae: - # ``register_buffer`` is typed as ``Tensor | Module`` on - # ``nn.Module``; narrow here since these are Tensors. - idf = cast(torch.Tensor, layer.idf) - max_val = cast(torch.Tensor, layer.max) - features = (features / max_val) * idf - - sae_outputs[model_name] = features.to_sparse() - - return sae_outputs - @can_return_tuple @auto_docstring def forward( @@ -760,8 +654,6 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -777,13 +669,6 @@ def forward( Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the attention probabilities are observable; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run any SAE models registered via :meth:`add_sae_models`. - Has no effect when no SAEs are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE feature magnitudes by ``idf / max`` (only - applied when the SAE's normalization buffers contain non-trivial values). - Examples: ```python @@ -803,26 +688,12 @@ def forward( output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions return_dict = return_dict if return_dict is not None else self.config.use_return_dict - output_sae = compute_sae and len(self._sae_models) > 0 - - # Determine which intermediate layers to collect. When SAEs are - # registered we must collect at least the layers they target, even if - # the caller did not ask for all hidden states. - if output_hidden_states: - layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) - elif output_sae: - layers_to_collect = sorted({self._get_sae_layer_num_requested(name) for name in self._sae_models}) - else: - layers_to_collect = [] + # Collect per-layer hidden states only when the caller asks for them. + layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) if output_hidden_states else [] user_supplied_sequence_id = sequence_id is not None - if user_supplied_sequence_id: - bool_mask = sequence_id >= 0 - else: - if attention_mask is None: - attention_mask = input_ids != self.config.pad_token_id - assert attention_mask is not None - bool_mask = attention_mask.bool() + if not user_supplied_sequence_id and attention_mask is None: + attention_mask = input_ids != self.config.pad_token_id x = self.embed(input_ids) position_ids = torch.arange(x.shape[1], device=x.device).unsqueeze(0) @@ -859,17 +730,9 @@ def forward( output_attentions=output_attentions, ) - # Stack once; reused for both SAE and hidden-state output. collected_tensor: torch.Tensor | None = ( torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] ) - - sae_outputs: dict[str, torch.Tensor] | None = None - if output_sae and collected_tensor is not None: - assert input_ids is not None - self._validate_sae_inputs(input_ids) - sae_outputs = self._get_sae_outputs(collected_tensor, layers_to_collect, bool_mask, normalize_sae) - hidden_states_tensor = collected_tensor if output_hidden_states else None if not return_dict: @@ -878,7 +741,6 @@ def forward( for v in [ last_hidden_state, hidden_states_tensor, - sae_outputs, attentions, ] if v is not None @@ -887,7 +749,6 @@ def forward( return ESMCOutput( last_hidden_state=last_hidden_state, hidden_states=hidden_states_tensor, - sae_outputs=sae_outputs, attentions=attentions, ) @@ -934,10 +795,6 @@ def get_output_embeddings(self) -> nn.Linear: def set_output_embeddings(self, new_embeddings: nn.Linear): self.lm_head[-1] = new_embeddings - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Proxy to :meth:`ESMCModel.add_sae_models`.""" - self.esmc.add_sae_models(sae_models) - @can_return_tuple @auto_docstring def forward( @@ -949,8 +806,6 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -962,10 +817,6 @@ def forward( output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run registered SAE models. Has no effect when none are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE features by ``idf / max`` normalization buffers. Examples: @@ -990,8 +841,6 @@ def forward( output_hidden_states=output_hidden_states, output_attentions=output_attentions, return_dict=True, - compute_sae=compute_sae, - normalize_sae=normalize_sae, ) logits = self.lm_head(encoder_outputs.last_hidden_state) @@ -1008,7 +857,6 @@ def forward( logits, encoder_outputs.last_hidden_state, encoder_outputs.hidden_states, - encoder_outputs.sae_outputs, encoder_outputs.attentions, ] if v is not None @@ -1019,7 +867,6 @@ def forward( logits=logits, last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, - sae_outputs=encoder_outputs.sae_outputs, attentions=encoder_outputs.attentions, ) @@ -1067,10 +914,6 @@ def __init__(self, config: ESMCConfig): self.classifier = _ESMCClassificationHead(config) self.post_init() - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Proxy to :meth:`ESMCModel.add_sae_models`.""" - self.esmc.add_sae_models(sae_models) - @can_return_tuple @auto_docstring def forward( @@ -1081,8 +924,6 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: r""" labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): @@ -1092,10 +933,6 @@ def forward( output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run registered SAE models. Has no effect when none are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE features by ``idf / max`` normalization buffers. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict @@ -1105,8 +942,6 @@ def forward( output_hidden_states=output_hidden_states, output_attentions=output_attentions, return_dict=True, - compute_sae=compute_sae, - normalize_sae=normalize_sae, ) logits = self.classifier(encoder_outputs.last_hidden_state) @@ -1141,7 +976,6 @@ def forward( logits, encoder_outputs.last_hidden_state, encoder_outputs.hidden_states, - encoder_outputs.sae_outputs, encoder_outputs.attentions, ] if v is not None @@ -1152,7 +986,6 @@ def forward( logits=logits, last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, - sae_outputs=encoder_outputs.sae_outputs, attentions=encoder_outputs.attentions, ) @@ -1178,10 +1011,6 @@ def __init__(self, config: ESMCConfig): self.classifier = nn.Linear(config.d_model, config.num_labels) self.post_init() - def add_sae_models(self, sae_models: list[_ESMCSAELayer]) -> None: - """Proxy to :meth:`ESMCModel.add_sae_models`.""" - self.esmc.add_sae_models(sae_models) - @can_return_tuple @auto_docstring def forward( @@ -1192,8 +1021,6 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, - compute_sae: bool = True, - normalize_sae: bool = False, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: r""" labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -1202,10 +1029,6 @@ def forward( output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. - compute_sae (`bool`, *optional*, defaults to ``True``): - Whether to run registered SAE models. Has no effect when none are registered. - normalize_sae (`bool`, *optional*, defaults to ``False``): - When ``True``, scale SAE features by ``idf / max`` normalization buffers. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict @@ -1215,8 +1038,6 @@ def forward( output_hidden_states=output_hidden_states, output_attentions=output_attentions, return_dict=True, - compute_sae=compute_sae, - normalize_sae=normalize_sae, ) sequence_output = self.dropout(encoder_outputs.last_hidden_state) @@ -1236,7 +1057,6 @@ def forward( logits, encoder_outputs.last_hidden_state, encoder_outputs.hidden_states, - encoder_outputs.sae_outputs, encoder_outputs.attentions, ] if v is not None @@ -1247,7 +1067,6 @@ def forward( logits=logits, last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, - sae_outputs=encoder_outputs.sae_outputs, attentions=encoder_outputs.attentions, ) From f42978a9010894cd2f2b32b065dda35a796d6a77 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 15:37:10 +0100 Subject: [PATCH 14/70] Normalize ESMC docstrings to satisfy check_docstrings `make fix-repo`/check_docstrings normalizations on the ESMC public objects: - ESMCTokenizer: default values use single backticks (`""`) per the canonical "*optional*, defaults to ..." format. - ESMCModel / ESMCForMaskedLM / ESMCForSequenceClassification: reorder the forward docstring argument entries to match the signature order (output_attentions before labels) and add the missing blank line before the Examples block. Docstring-only (no logic change); modular and generated modeling stay in sync. ESMC now passes check_repo, check_copies, check_inits, check_dummies, check_modular_conversion, and check_docstrings (remaining repo-wide failures are pre-existing Qwen output-doc errors and Phase-B esmfold2, neither ESMC). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmc/modeling_esmc.py | 19 ++++++++++--------- src/transformers/models/esmc/modular_esmc.py | 19 ++++++++++--------- .../models/esmc/tokenization_esmc.py | 12 ++++++------ 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index a1ffd1b62858..fa2cae80cfbe 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -726,6 +726,7 @@ def forward( Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the attention probabilities are observable; raises on the ``flash_attention_2`` path. + Examples: ```python @@ -868,12 +869,12 @@ def forward( sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor forwarded to the encoder for chain-aware attention masking. See :meth:`ESMCModel.forward` for the encoding. - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for masked language modelling loss. Positions with label ``-100`` - are ignored. Other positions must be in ``[0, config.vocab_size)``. output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for masked language modelling loss. Positions with label ``-100`` + are ignored. Other positions must be in ``[0, config.vocab_size)``. Examples: @@ -983,13 +984,13 @@ def forward( labels: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: r""" + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): Labels for sequence classification loss. Indices must be in ``[0, config.num_labels - 1]``. For regression pass a float tensor of shape ``(batch_size,)``. - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict @@ -1080,12 +1081,12 @@ def forward( labels: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. - Positions with index ``-100`` are ignored in the loss. output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. + Positions with index ``-100`` are ignored in the loss. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 2440dd0412f2..3164b6f73ee3 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -669,6 +669,7 @@ def forward( Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the attention probabilities are observable; raises on the ``flash_attention_2`` path. + Examples: ```python @@ -811,12 +812,12 @@ def forward( sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor forwarded to the encoder for chain-aware attention masking. See :meth:`ESMCModel.forward` for the encoding. - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Labels for masked language modelling loss. Positions with label ``-100`` - are ignored. Other positions must be in ``[0, config.vocab_size)``. output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for masked language modelling loss. Positions with label ``-100`` + are ignored. Other positions must be in ``[0, config.vocab_size)``. Examples: @@ -926,13 +927,13 @@ def forward( labels: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: r""" + output_attentions (`bool`, *optional*): + Whether to return per-block attention weights. Forwarded to the + backbone; raises on the ``flash_attention_2`` path. labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): Labels for sequence classification loss. Indices must be in ``[0, config.num_labels - 1]``. For regression pass a float tensor of shape ``(batch_size,)``. - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict @@ -1023,12 +1024,12 @@ def forward( labels: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: r""" - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. - Positions with index ``-100`` are ignored in the loss. output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the backbone; raises on the ``flash_attention_2`` path. + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. + Positions with index ``-100`` are ignored in the loss. """ return_dict = return_dict if return_dict is not None else self.config.use_return_dict diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index e5fb07813f2f..73fbe46aab57 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -73,17 +73,17 @@ class ESMCTokenizer(PreTrainedTokenizerFast): token for multi-chain inputs. Args: - unk_token (`str`, *optional*, defaults to ``""``): + unk_token (`str`, *optional*, defaults to `""`): The unknown token. - cls_token (`str`, *optional*, defaults to ``""``): + cls_token (`str`, *optional*, defaults to `""`): The classification token (prepended to every sequence). - pad_token (`str`, *optional*, defaults to ``""``): + pad_token (`str`, *optional*, defaults to `""`): The padding token. - mask_token (`str`, *optional*, defaults to ``""``): + mask_token (`str`, *optional*, defaults to `""`): The mask token, used for masked language modelling. - eos_token (`str`, *optional*, defaults to ``""``): + eos_token (`str`, *optional*, defaults to `""`): The end-of-sequence token (appended to every sequence). - chain_break_token (`str`, *optional*, defaults to ``"|"``): + chain_break_token (`str`, *optional*, defaults to `"|"`): Token inserted between chains in multi-chain protein inputs. Examples: From e08219307fe81a8884ce591b9d06630ce419b0fb Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 15:59:52 +0100 Subject: [PATCH 15/70] Get ESMFold2 importing: add __all__ + load ESMC via the Auto registry - Add `__all__ = ["ESMFold2Model"]` to modeling_esmfold2.py so the model is exported (`from transformers import ESMFold2Model` now works; previously the class was defined but the module had no `__all__`). - Replace the hard cross-model import `from ..esmc.modeling_esmc import ESMCModel` in `load_esmc` (both modeling_esmfold2.py and the experimental file) with `AutoModel.from_pretrained(...)`. ESMC is a shared, frozen 6B backbone loaded separately from its own repo (`config.esmc_id`); resolving it through the Auto registry (model_type "esmc" -> ESMCModel) keeps esmc and esmfold2 as separate model directories without a runtime cross-dir import. Verified: ESMFold2Model + ESMFold2Config export; core + experimental modules import; no `..esmc` imports remain in the esmfold2 dir; AutoModel.from_config resolves ESMCConfig -> ESMCModel. (Also removed the empty kernels/ and distributed/ dirs left behind by their earlier git rm.) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmfold2/modeling_esmfold2.py | 10 ++++++++-- .../models/esmfold2/modeling_esmfold2_experimental.py | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 17f1e39b1d7a..927ee22eff12 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -586,7 +586,10 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: ``"fp8"`` requires H100 + TransformerEngine ≥ 2.x and quantizes every TE module's weights to fp8 storage. """ - from ..esmc.modeling_esmc import ESMCModel + # Resolve the ESMC backbone through the Auto registry (model_type "esmc" + # -> ESMCModel) rather than a hard cross-model import. ESMC is a shared, + # frozen backbone loaded from its own repo (`esmc_id`), not bundled here. + from ...models.auto.modeling_auto import AutoModel dtype_map = { "bf16": torch.bfloat16, @@ -600,7 +603,7 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: dtype = dtype_map[precision] esmc = ( - ESMCModel.from_pretrained(esmc_model_path) + AutoModel.from_pretrained(esmc_model_path) .to(device=self.device, dtype=dtype) .eval() ) @@ -1137,3 +1140,6 @@ def forward( for block in self.blocks: m, x_pair = block(m, x_pair, msa_attention_mask, pair_attention_mask) return x_pair + + +__all__ = ["ESMFold2Model"] diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py index 8edc2b6b7b04..a18d7c27a542 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py @@ -586,9 +586,11 @@ def configure_lm_dropout( def load_esmc(self, esmc_model_path: str) -> None: """Load the ESMC LM backbone from a HuggingFace Hub repo ID or local directory.""" - from ..esmc.modeling_esmc import ESMCModel # type: ignore[import] + # Resolve via the Auto registry (model_type "esmc" -> ESMCModel) rather + # than a hard cross-model import. + from ...models.auto.modeling_auto import AutoModel - esmc = ESMCModel.from_pretrained(esmc_model_path) + esmc = AutoModel.from_pretrained(esmc_model_path) self._esmc = esmc.bfloat16().to(self.device).eval() @classmethod From 4a435e0d8723ab492626c2cec4f59c824acca73d Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 16:04:56 +0100 Subject: [PATCH 16/70] Drop the experimental ESMFold2 variant + esmfold2_v2 mapping (release-only) Scope the core ESMFold2 PR to the release model. The experimental (legacy/dev binder-design) variant is deferred like the ESMC SAE. - Delete modeling_esmfold2_experimental.py and remove it from the package __init__ and from the ESMFold2Model.from_pretrained `config.type == "experimental"` routing (from_pretrained now always builds ESMFold2Model). - ESMFold2Config: keep the `type` field for checkpoint compat but accept only "release"; fix the docstring example to use ESMFold2Model. Update the two stale `ESMFold2ExperimentalModel` references in comments/docstrings. - Remove the `esmfold2_v2 -> esmfold2` SPECIAL_MODEL_TYPE_TO_MODULE_NAME entry (no v2 variant ships). Verified: ESMFold2Model + ESMFold2Config import; ESMFold2ExperimentalModel gone; no experimental/v2 references remain. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/auto/auto_mappings.py | 1 - src/transformers/models/esmfold2/__init__.py | 1 - .../models/esmfold2/configuration_esmfold2.py | 14 +- .../models/esmfold2/modeling_esmfold2.py | 10 - .../esmfold2/modeling_esmfold2_common.py | 2 +- .../modeling_esmfold2_experimental.py | 1051 ----------------- .../models/esmfold2/protein_utils.py | 2 +- 7 files changed, 9 insertions(+), 1072 deletions(-) delete mode 100644 src/transformers/models/esmfold2/modeling_esmfold2_experimental.py diff --git a/src/transformers/models/auto/auto_mappings.py b/src/transformers/models/auto/auto_mappings.py index a850f926a5cd..12e63aea35dc 100644 --- a/src/transformers/models/auto/auto_mappings.py +++ b/src/transformers/models/auto/auto_mappings.py @@ -743,7 +743,6 @@ ("encoder-decoder", "encoder_decoder"), ("ernie4_5_vl_moe_text", "ernie4_5_vl_moe"), ("ernie4_5_vl_moe_vision", "ernie4_5_vl_moe"), - ("esmfold2_v2", "esmfold2"), ("exaone4_5_vision", "exaone4_5"), ("fastspeech2_conformer_hifigan", "fastspeech2_conformer"), ("fastspeech2_conformer_with_hifigan", "fastspeech2_conformer"), diff --git a/src/transformers/models/esmfold2/__init__.py b/src/transformers/models/esmfold2/__init__.py index 6b245e85ea05..1b4fd78e45c6 100644 --- a/src/transformers/models/esmfold2/__init__.py +++ b/src/transformers/models/esmfold2/__init__.py @@ -19,7 +19,6 @@ if TYPE_CHECKING: from .configuration_esmfold2 import * # noqa: F403 from .modeling_esmfold2 import * # noqa: F403 - from .modeling_esmfold2_experimental import * # noqa: F403 else: import sys diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 2a8a215cf9a8..4f0548dc10aa 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -211,13 +211,13 @@ class ESMFold2Config(PretrainedConfig): Examples: ```python - >>> from transformers import ESMFold2Config, ESMFold2ExperimentalModel + >>> from transformers import ESMFold2Config, ESMFold2Model >>> # Initializing an ESMFold2 configuration - >>> configuration = ESMFold2Config(type="experimental") + >>> configuration = ESMFold2Config(type="release") >>> # Initializing a model (with random weights) from the configuration - >>> model = ESMFold2ExperimentalModel(configuration) + >>> model = ESMFold2Model(configuration) >>> # Accessing the model configuration >>> configuration = model.config @@ -230,11 +230,11 @@ class ESMFold2Config(PretrainedConfig): def __init__(self, **kwargs): super().__init__(**kwargs) - self.type: str = kwargs["type"] - if self.type not in ("release", "experimental"): + self.type: str = kwargs.get("type", "release") + if self.type != "release": raise ValueError( - f"ESMFold2Config.type must be 'release' or 'experimental', " - f"got {self.type!r}" + "ESMFold2Config.type must be 'release' (the 'experimental' variant " + f"is not included in this release), got {self.type!r}" ) # Top-level scalar fields diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 927ee22eff12..530ecbc361ac 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -631,16 +631,6 @@ def from_pretrained( config = ESMFold2Config.from_pretrained( pretrained_model_name_or_path, **kwargs ) - if config.type == "experimental": - from .modeling_esmfold2_experimental import ESMFold2ExperimentalModel - - return ESMFold2ExperimentalModel.from_pretrained( - pretrained_model_name_or_path, - *args, - config=config, - load_esmc=load_esmc, - **kwargs, - ) kwargs["config"] = config # Pop the precision knob before forwarding to the HF loader. esmc_precision = kwargs.pop("esmc_precision", "bf16") diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 1f2e0454ecfc..7a6ae88912b3 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -1979,7 +1979,7 @@ def _seed_context(seed: int | None, *, cuda: bool = True): # =========================================================================== -# ESMFold2ExperimentalModel — the top-level PreTrainedModel +# ESMFold2 — language-model backbone helpers # =========================================================================== diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py b/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py deleted file mode 100644 index a18d7c27a542..000000000000 --- a/src/transformers/models/esmfold2/modeling_esmfold2_experimental.py +++ /dev/null @@ -1,1051 +0,0 @@ -# coding=utf-8 -# Copyright 2026 Biohub. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""ESMFold2 experimental variant — older architecture from during the -development of ESMFold2. - -Most users want :class:`ESMFold2Model` (in ``modeling_esmfold2``) instead; -this module retains an explicit refinement loop that re-injects the -previous pair representation through ``pair_loop_proj`` each iteration, -and exists to load checkpoints predating the standard architecture. -""" - -from __future__ import annotations - -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch import Tensor - -from ...modeling_utils import PreTrainedModel # type: ignore[import] -from .configuration_esmfold2 import ESMFold2Config -from .modeling_esmfold2_common import ( - CHAR_VOCAB_SIZE, - MAX_ATOMIC_NUMBER, - NUM_RES_TYPES, - DiffusionStructureHead, - FoldingTrunk, - InputsEmbedder, - LanguageModelShim, - MSAPairWeightedAveraging, - OuterProductMean, - ResIdxAsymIdSymIdEntityIdEncoding, - RowAttentionPooling, - SwiGLUMLP, - TriangleMultiplicativeUpdate, - _categorical_mean, - _compute_intra_token_idx, - _seed_context, - compute_lm_hidden_states, - gather_rep_atom_coords, - gather_token_to_atom, -) - -_EPS = 1e-5 -_NONPOLYMER_ID: int = 3 - - -# =========================================================================== -# ConfidenceHead -# =========================================================================== - - -class ConfidenceHead(nn.Module): - """Confidence head predicting per-atom pLDDT and pairwise PAE.""" - - boundaries: Tensor - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__() - ch = config.confidence_head - d_single = config.d_single - d_pair = config.d_pair - d_inputs = config.inputs.d_inputs - - # Distogram bins boundary buffer - boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1) - self.register_buffer("boundaries", boundaries) - - self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) - - self.s_norm = nn.LayerNorm(d_single) - - self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) - - self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) - - # s_to_z_prod - self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) - - # s_input_to_s - self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) - - self.s_inputs_norm = nn.LayerNorm(d_inputs) - self.z_norm = nn.LayerNorm(d_pair) - - # Row attention pooling - self.row_attention_pooling = RowAttentionPooling( - d_pair=d_pair, d_single=d_single - ) - - # Confidence folding trunk (4 blocks) - pf = ch.folding_trunk - self.folding_trunk = FoldingTrunk( - n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 - ) - - # pLDDT head - self.plddt_ln = nn.LayerNorm(d_single) - max_atoms_per_token = 23 - self.plddt_weight = nn.Parameter( - torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins) - ) - - # PAE head - self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) - - # ------------------------------------------------------------------ - # Kernel / chunking configuration - # ------------------------------------------------------------------ - - def set_chunk_size(self, chunk_size: int | None) -> None: - self.folding_trunk.set_chunk_size(chunk_size) - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - @staticmethod - def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor: - if num_diffusion_samples == 1: - return x - return x.repeat_interleave(num_diffusion_samples, 0) - - @staticmethod - def _flatten_sample_axis(x: Tensor) -> Tensor: - if x.ndim == 4: - b, mult, n, c = x.shape - return x.reshape(b * mult, n, c) - return x - - # ------------------------------------------------------------------ - # Forward - # ------------------------------------------------------------------ - - def forward( - self, - s_inputs: Tensor, - z: Tensor, - x_pred: Tensor, - distogram_atom_idx: Tensor, - token_attention_mask: Tensor, - atom_to_token: Tensor, - atom_attention_mask: Tensor, - asym_id: Tensor, - mol_type: Tensor, - num_diffusion_samples: int = 1, - relative_position_encoding: Tensor | None = None, - token_bonds_encoding: Tensor | None = None, - ) -> dict[str, Tensor]: - """Run confidence head.""" - # Shared computation (batch-scale, before num_diffusion_samples expansion) - s_inputs_normed = self.s_inputs_norm(s_inputs) - - z_base = self.z_norm(z) - if relative_position_encoding is not None: - z_base = z_base + relative_position_encoding - if token_bonds_encoding is not None: - z_base = z_base + token_bonds_encoding - z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2) - z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1) - z_base = z_base + self.s_to_z_prod_out( - self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] - * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] - ) - - # Expand to num_diffusion_samples - pair = self._repeat_batch(z_base, num_diffusion_samples) - x_pred_flat = self._flatten_sample_axis(x_pred) - atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples) - atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples) - rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long() - mask = self._repeat_batch(token_attention_mask, num_diffusion_samples) - Bm = pair.shape[0] - - # Distogram from predicted coords - rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) - rep_distances = torch.cdist( - rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist" - ) - distogram_bins = ( - (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() - ) - pair = pair + self.dist_bin_pairwise_embed(distogram_bins) - - # Expand 1-D token mask → 2-D pair mask for folding trunk - pair_mask = mask[:, :, None].float() * mask[:, None, :].float() # [B*m, L, L] - - # FoldingTrunk + row attention pooling -> single - pair = pair + self.folding_trunk(pair, pair_attention_mask=pair_mask) - single = self.row_attention_pooling(pair, mask) - - # Per-atom pLDDT - atom_mask_f = atom_mask_m.float() - s_at_atoms = gather_token_to_atom(single, atom_to_token_m) - s_at_atoms = self.plddt_ln(s_at_atoms) - - intra_idx = _compute_intra_token_idx(atom_to_token_m) - intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) - w = self.plddt_weight[intra_idx] # [B*m, A, d_single, num_bins] - plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms, w) - - plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0) - - # Per-token pLDDT (scatter mean) - L = single.shape[1] - plddt_sum = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) - atom_count = torch.zeros( - Bm, L, device=single.device, dtype=plddt_per_atom.dtype - ) - atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype) - plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t) - atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) - plddt = plddt_sum / atom_count.clamp(min=1e-6) - - # Complex pLDDT (flat mean over all atoms) - complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / ( - atom_mask_f.sum(dim=-1) + _EPS - ) - - # Complex ipLDDT (interface-weighted) - expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) - expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) - is_ligand = (expanded_type == _NONPOLYMER_ID).float() - inter_chain = ( - expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) - ).float() - near_contact = (rep_distances < 8).float() - interface_per_token = ( - near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1) - ).amax(dim=-1) - iplddt_weight = torch.where( - is_ligand.bool(), - torch.full_like(interface_per_token, 2.0), - interface_per_token, - ) - iplddt_weight_atoms = gather_token_to_atom( - iplddt_weight.unsqueeze(-1), atom_to_token_m - ).squeeze(-1) - atom_iplddt_w = atom_mask_f * iplddt_weight_atoms - complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / ( - atom_iplddt_w.sum(dim=-1) + _EPS - ) - - # pLDDT at CA / representative atom - plddt_ca = plddt_per_atom.gather(1, rep_idx_m) - - # PAE - pae_logits = self.pae_head(pair) - pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() - - # pTM / ipTM / per-chain-pair ipTM derived from pae_logits. - n_bins = pae_logits.shape[-1] - bin_width = 32.0 / n_bins - bin_centers = torch.arange( - 0.5 * bin_width, 32.0, bin_width, device=pae_logits.device - ) - mask_f = mask.float() - N_res = mask_f.sum(dim=-1, keepdim=True) - d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 # [Bm, 1] - tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2) # [Bm, n_bins] - pae_probs = F.softmax(pae_logits, dim=-1) - tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum( - dim=-1 - ) # [Bm, L, L] - - pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) # [Bm, L, L] - - # pTM: avg over all valid pairs per row, max over rows. - ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / ( - pair_mask_2d.sum(dim=-1) + _EPS - ) - ptm = ptm_per_row.max(dim=-1).values # [Bm] - - # ipTM: avg over inter-chain valid pairs per row, max over rows. - inter_chain_mask = ( - expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) - ).float() * pair_mask_2d - iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / ( - inter_chain_mask.sum(dim=-1) + _EPS - ) - iptm = iptm_per_row.max(dim=-1).values # [Bm] - - # Per-chain-pair ipTM: dense [Bm, N_chains, N_chains] padded to max chain id + 1. - max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 - n_chains = max_chain_id + 1 - pair_chains_iptm = torch.zeros( - Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype - ) - for c1 in range(n_chains): - chain_c1 = (expanded_asym == c1).float() * mask_f - if chain_c1.sum() == 0: - continue - for c2 in range(n_chains): - chain_c2 = (expanded_asym == c2).float() * mask_f - pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) - denom = pair_m.sum(dim=(-1, -2)) + _EPS - pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum( - dim=(-1, -2) - ) / denom - - return { - "plddt_logits": plddt_logits, - "plddt": plddt.detach(), - "plddt_per_atom": plddt_per_atom.detach(), - "plddt_ca": plddt_ca.detach(), - "complex_plddt": complex_plddt.detach(), - "complex_iplddt": complex_iplddt.detach(), - "pae_logits": pae_logits, - "pae": pae, - "ptm": ptm.detach(), - "iptm": iptm.detach(), - "pair_chains_iptm": pair_chains_iptm.detach(), - } - - -# =========================================================================== -# MSA Encoder -# =========================================================================== - - -class _TransitionFFN(nn.Module): - """LayerNorm + SwiGLU FFN without residual (used inside MSAEncoderBlock).""" - - def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: - super().__init__() - self.norm = nn.LayerNorm(d_model) - self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) - - def forward(self, x: Tensor) -> Tensor: - return self.ffn(self.norm(x)) - - -class MSAEncoderBlock(nn.Module): - """One block of the MSA encoder: MSA update + pair update.""" - - def __init__( - self, - d_msa: int, - d_pair: int, - d_hidden: int = 32, - n_heads_msa: int = 8, - msa_head_width: int = 32, - ) -> None: - super().__init__() - self.outer_product_mean = OuterProductMean( - d_msa, d_hidden, d_pair, divide_outer_before_proj=True - ) - self.msa_pair_weighted_averaging = MSAPairWeightedAveraging( - d_msa, d_pair, n_heads_msa, msa_head_width - ) - self.msa_transition = _TransitionFFN(d_msa, expansion_ratio=4) - self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) - self.pair_transition = _TransitionFFN(d_pair, expansion_ratio=4) - - def forward( - self, - msa_repr: Tensor, - pair_repr: Tensor, - msa_attention_mask: Tensor, - pair_attention_mask: Tensor, - msa_track_mask: Tensor | None = None, - ) -> tuple[Tensor, Tensor]: - """ - Args: - msa_repr: [B, L, M, d_msa] - pair_repr: [B, L, L, d_pair] - msa_attention_mask: [B, L, M] - pair_attention_mask:[B, L, L] - msa_track_mask: [B] bool — if False for a sample, zero out its contribution - Returns: - (msa_repr, pair_repr) - """ - mask4d = ( - msa_track_mask[:, None, None, None].to(dtype=msa_repr.dtype) - if msa_track_mask is not None - else None - ) - - def _maybe_mask(x: Tensor) -> Tensor: - return x * mask4d if mask4d is not None else x - - msa_repr = msa_repr + _maybe_mask( - self.msa_pair_weighted_averaging(msa_repr, pair_repr, pair_attention_mask) - ) - msa_repr = msa_repr + _maybe_mask(self.msa_transition(msa_repr)) - - pair_repr = pair_repr + _maybe_mask( - self.outer_product_mean(msa_repr, msa_attention_mask) - ) - pair_repr = pair_repr + _maybe_mask( - self.tri_mul_out(pair_repr, mask=pair_attention_mask) - ) - pair_repr = pair_repr + _maybe_mask( - self.tri_mul_in(pair_repr, mask=pair_attention_mask) - ) - pair_repr = pair_repr + _maybe_mask(self.pair_transition(pair_repr)) - - return msa_repr, pair_repr - - -class MSAEncoder(nn.Module): - """Embeds MSA features and runs encoder blocks to update the pair representation.""" - - def __init__( - self, - d_msa: int, - d_pair: int, - d_inputs: int, - d_hidden: int = 32, - n_layers: int = 4, - n_heads_msa: int = 8, - msa_head_width: int = 32, - ) -> None: - super().__init__() - # 33 aa one-hot + has_deletion + deletion_value = 35 - self.embed = nn.Linear(35, d_msa, bias=False) - self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) - self.blocks = nn.ModuleList( - [ - MSAEncoderBlock( - d_msa=d_msa, - d_pair=d_pair, - d_hidden=d_hidden, - n_heads_msa=n_heads_msa, - msa_head_width=msa_head_width, - ) - for _ in range(n_layers) - ] - ) - - def forward( - self, - x_pair: Tensor, - x_inputs: Tensor, - msa_oh: Tensor, - has_deletion: Tensor, - deletion_value: Tensor, - msa_attention_mask: Tensor, - ) -> Tensor: - """ - Args: - x_pair: [B, L, L, d_pair] current pair representation - x_inputs: [B, L, d_inputs] per-token input features - msa_oh: [B, L, M, 33] one-hot MSA (already transposed) - has_deletion: [B, L, M] - deletion_value: [B, L, M] - msa_attention_mask:[B, L, M] - Returns: - [B, L, L, d_pair] pair update - """ - B, L, M = msa_attention_mask.shape - - m_feat = torch.cat( - [msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1 - ) - m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2) - - # Mask out the full update for samples with no real non-query MSA rows. - if M > 1: - msa_track_mask = msa_attention_mask[:, :, 1:].any(dim=(1, 2)) - else: - msa_track_mask = torch.zeros(B, dtype=torch.bool, device=x_pair.device) - - tok_mask = msa_attention_mask[:, :, 0] - pair_attention_mask = tok_mask.unsqueeze(2) * tok_mask.unsqueeze(1) - - for block in self.blocks: - m, x_pair = block( - m, x_pair, msa_attention_mask, pair_attention_mask, msa_track_mask - ) - - x_pair = x_pair * msa_track_mask[:, None, None, None].to(dtype=x_pair.dtype) - return x_pair - - -# =========================================================================== -# ESMFold2ExperimentalModel — the top-level PreTrainedModel -# =========================================================================== - - -class ESMFold2ExperimentalModel(PreTrainedModel): - """ESMFold2 v2 structure prediction model.""" - - config_class = ESMFold2Config - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__(config) - - # InputsEmbedder - self.inputs_embedder = InputsEmbedder(config) - - # z_init projections - d_inputs = config.inputs.d_inputs - d_pair = config.d_pair - - self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) - self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False) - - # Trunk relative position encoding - self.rel_pos = ResIdxAsymIdSymIdEntityIdEncoding( - n_relative_residx_bins=config.n_relative_residx_bins, - n_relative_chain_bins=config.n_relative_chain_bins, - d_pair=d_pair, - ) - - # Token bonds - self.token_bonds = nn.Linear(1, d_pair, bias=False) - - self.language_model = LanguageModelShim( - d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers - ) - self._esmc: nn.Module | None = None # lazily loaded - - # FoldingTrunk - pf = config.folding_trunk - self.folding_trunk = FoldingTrunk( - n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 - ) - - # Per-loop pair re-injection projection - self.pair_loop_proj = nn.Sequential( - nn.LayerNorm(d_pair), nn.Linear(d_pair, d_pair, bias=False) - ) - nn.init.zeros_(self.pair_loop_proj[1].weight) # type: ignore[arg-type] - - # Structure head - self.structure_head = DiffusionStructureHead(config) - - # Distogram head - self.distogram_head = nn.Linear( - d_pair, config.structure_head.distogram_bins, bias=True - ) - - if config.confidence_head.enabled: - self.confidence_head: ConfidenceHead | None = ConfidenceHead(config) - else: - self.confidence_head = None - - # MSA encoder (Large MSA models only) - msa_cfg = config.msa_encoder - if msa_cfg.enabled: - self.msa_encoder: MSAEncoder | None = MSAEncoder( - d_msa=msa_cfg.d_msa, - d_pair=d_pair, - d_inputs=d_inputs, - d_hidden=msa_cfg.d_hidden, - n_layers=msa_cfg.n_layers, - n_heads_msa=msa_cfg.n_heads_msa, - msa_head_width=msa_cfg.msa_head_width, - ) - else: - self.msa_encoder = None - - self.post_init() - - def set_chunk_size(self, chunk_size: int | None) -> None: - """Set chunk size for memory-efficient triangle multiplicative updates.""" - self.folding_trunk.set_chunk_size(chunk_size) - if self.confidence_head is not None: - self.confidence_head.set_chunk_size(chunk_size) - - def configure_lm_dropout( - self, lm_dropout: float, *, force_lm_dropout_during_inference: bool = True - ) -> None: - """Configure LM embedding dropout (binder-design / critic scoring).""" - self.config.lm_dropout = lm_dropout - self.config.force_lm_dropout_during_inference = ( - force_lm_dropout_during_inference - ) - - def load_esmc(self, esmc_model_path: str) -> None: - """Load the ESMC LM backbone from a HuggingFace Hub repo ID or local directory.""" - # Resolve via the Auto registry (model_type "esmc" -> ESMCModel) rather - # than a hard cross-model import. - from ...models.auto.modeling_auto import AutoModel - - esmc = AutoModel.from_pretrained(esmc_model_path) - self._esmc = esmc.bfloat16().to(self.device).eval() - - @classmethod - def from_pretrained( - cls, pretrained_model_name_or_path, *args, load_esmc: bool = True, **kwargs - ): - model = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs) - if load_esmc: - model.load_esmc(model.config.esmc_id) - return model - - @torch.no_grad() - def infer_protein(self, seq: str, **forward_kwargs) -> dict: - from .protein_utils import OUTPUT_TO_PDB_FEATURE_KEYS, prepare_protein_features - - features = prepare_protein_features(seq) - features = {k: v.to(self.device) for k, v in features.items()} - output = self(**features, **forward_kwargs) - for k in OUTPUT_TO_PDB_FEATURE_KEYS: - output[k] = features[k] - return output - - def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str: - return self.output_to_pdb(self.infer_protein(seq, **forward_kwargs)) - - @torch.no_grad() - def infer_all_atom(self, structure_input, **forward_kwargs): - try: - import evolutionaryscale.opensource as esm # TODO: change to `import esm` when open sourcing - import evolutionaryscale.opensource.models.esmfold2 # noqa: F401 # TODO: drop when open sourcing - except ImportError as e: - raise NotImplementedError( - "All-atom inference requires the `esm` companion package: " - "`pip install esm`." - ) from e - esmfold2 = esm.models.esmfold2 # type: ignore[attr-defined] - - if isinstance(structure_input, esmfold2.ProteinInput): - structure_input = esmfold2.StructurePredictionInput( - sequences=[structure_input] - ) - processor = esmfold2.ESMFold2InputBuilder() - features, chain_infos = processor.prepare_input(structure_input) - features = { - k: v.to(self.device) if isinstance(v, Tensor) else v - for k, v in features.items() - } - output = self(**features, **forward_kwargs) - return self._output_to_molecular_complex(output, features, chain_infos) - - @staticmethod - def _output_to_molecular_complex(output: dict, features: dict, chain_infos: list): - import evolutionaryscale.opensource as esm # TODO: change to `import esm` when open sourcing - import evolutionaryscale.opensource.models.esmfold2 # noqa: F401 # TODO: drop when open sourcing - - esmfold2 = esm.models.esmfold2 # type: ignore[attr-defined] - - ELEMENT_NUMBER_TO_SYMBOL = esmfold2.ELEMENT_NUMBER_TO_SYMBOL - MolecularComplex = esmfold2.MolecularComplex - MolecularComplexMetadata = esmfold2.MolecularComplexMetadata - - coords = output["sample_atom_coords"] - if coords.dim() == 4: - coords = coords[:, 0] - coords_np = coords.detach().cpu().numpy() - - plddt = output["plddt"].detach().cpu().numpy() - atom_to_token = features["atom_to_token"].cpu().numpy() - ref_chars = features["ref_atom_name_chars"].cpu().numpy() - ref_element = features["ref_element"].cpu().numpy() - atom_mask = features["atom_attention_mask"].cpu().numpy().astype(bool) - - if atom_to_token.ndim == 1: - atom_to_token = atom_to_token[None] - ref_chars = ref_chars[None] - ref_element = ref_element[None] - atom_mask = atom_mask[None] - - b = 0 - atoms_by_token: dict[int, list[int]] = {} - for a in range(atom_to_token.shape[1]): - if not atom_mask[b, a]: - continue - atoms_by_token.setdefault(int(atom_to_token[b, a]), []).append(a) - - flat_positions: list[np.ndarray] = [] - flat_elements: list[str] = [] - flat_names: list[str] = [] - flat_hetero: list[bool] = [] - sequence_tokens: list[str] = [] - token_to_atoms: list[list[int]] = [] - chain_ids_per_token: list[int] = [] - confidence_scores: list[float] = [] - chain_lookup: dict[int, str] = {} - entity_info: dict[int, str] = {} - - cursor = 0 - for chain in chain_infos: - chain_lookup[chain.asym_id] = chain.chain_id - entity_info[chain.entity_id] = ( - "polymer" if chain.mol_type != 3 else "non-polymer" - ) - for tok in chain.tokens: - sequence_tokens.append(tok.residue_name) - chain_ids_per_token.append(chain.asym_id) - confidence_scores.append(float(plddt[b, tok.token_index])) - start = cursor - for a in atoms_by_token.get(tok.token_index, []): - flat_positions.append(coords_np[b, a]) - name = "".join( - chr(int(c) + 32) if int(c) != 0 else " " - for c in ref_chars[b, a] - ).strip() - flat_names.append(name) - flat_elements.append( - ELEMENT_NUMBER_TO_SYMBOL.get(int(ref_element[b, a]), "X") - ) - flat_hetero.append(chain.mol_type == 3) - cursor += 1 - token_to_atoms.append([start, cursor]) - - return MolecularComplex( - id="prediction", - sequence=sequence_tokens, - atom_positions=np.array(flat_positions, dtype=np.float32), - atom_elements=np.array(flat_elements, dtype=object), - token_to_atoms=np.array(token_to_atoms, dtype=np.int32), - chain_id=np.array(chain_ids_per_token, dtype=np.int64), - plddt=np.array(confidence_scores, dtype=np.float32), - atom_names=np.array(flat_names, dtype=object), - atom_hetero=np.array(flat_hetero, dtype=bool), - metadata=MolecularComplexMetadata( - entity_lookup={k: str(v) for k, v in entity_info.items()}, - chain_lookup=chain_lookup, - assembly_composition=None, - ), - ) - - @staticmethod - def output_to_pdb(output: dict) -> str: - from .protein_utils import output_to_pdb as _output_to_pdb - - return _output_to_pdb(output) - - def _compute_lm_hidden_states( - self, - input_ids: Tensor, - asym_id: Tensor, - residue_index: Tensor, - mol_type: Tensor, - token_mask: Tensor, - ) -> Tensor: - """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers.""" - assert self._esmc is not None - return compute_lm_hidden_states( - self._esmc, input_ids, asym_id, residue_index, mol_type, token_mask - ) - - def forward( - self, - # Token features - token_index: Tensor, - residue_index: Tensor, - asym_id: Tensor, - sym_id: Tensor, - entity_id: Tensor, - mol_type: Tensor, - res_type: Tensor, - token_bonds: Tensor, - token_attention_mask: Tensor, - # Atom features - ref_pos: Tensor, - ref_element: Tensor, - ref_charge: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - atom_attention_mask: Tensor, - atom_to_token: Tensor, - distogram_atom_idx: Tensor, - # MSA features - deletion_mean: Tensor | None = None, - msa: Tensor | None = None, - has_deletion: Tensor | None = None, - deletion_value: Tensor | None = None, - msa_attention_mask: Tensor | None = None, - # LM features (auto-computed from input_ids if ESMC loaded) - input_ids: Tensor | None = None, - lm_hidden_states: Tensor | None = None, - # Used in design to provide a soft sequence input. - res_type_soft: Tensor | None = None, - # Inference config - num_loops: int | None = None, - num_diffusion_samples: int | None = None, - num_sampling_steps: int | None = None, - early_exit: bool = False, - seed: int | None = None, - **kwargs, - ) -> dict[str, Tensor]: - """Full ESMFold2 inference pipeline. - - Accepts tensors directly from ESMFold2InputBuilder.prepare_input(). - - Returns: - dict with sample_atom_coords, plddt, pae, distogram_logits, etc. - """ - tok_mask = token_attention_mask - atm_mask = atom_attention_mask - disto_idx = distogram_atom_idx - - n_loops: int = num_loops if num_loops is not None else self.config.num_loops - n_samples: int = ( - num_diffusion_samples - if num_diffusion_samples is not None - else self.config.num_diffusion_samples - ) - - # One-hot res_type for input embedder concatenation - if res_type.dim() == 2: - res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float() - res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() - else: - res_type_oh = res_type.float() - - # Profile: masked mean over MSA depth, with res_type fallback when no MSA. - if msa is not None: - msa_oh_profile = F.one_hot( - msa.long(), num_classes=NUM_RES_TYPES - ).float() # [B, M, L, V] - if msa_attention_mask is not None: - mask_f = msa_attention_mask.float().unsqueeze(-1) # [B, M, L, 1] - msa_oh_profile = msa_oh_profile * mask_f - valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) - profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) - else: - profile = msa_oh_profile.mean(dim=1) - else: - profile = res_type_oh - - # Used in design to provide a soft sequence input. - if res_type_soft is not None: - res_type_oh = res_type_soft.float() - if not getattr(self.config, "disable_msa_features", False) and kwargs.get( - "provide_soft_sequence_to_msa_and_profile", True - ): - profile = res_type_oh - msa = res_type_oh.unsqueeze(1) - msa_attention_mask = tok_mask.unsqueeze(1) - - if deletion_mean is None: - deletion_mean = torch.zeros( - res_type.shape[0], res_type.shape[1], device=res_type.device - ) - - if getattr(self.config, "disable_msa_features", False): - profile = torch.zeros_like(profile) - deletion_mean = torch.zeros_like(deletion_mean) - - ref_element = F.one_hot( - ref_element.long(), num_classes=MAX_ATOMIC_NUMBER - ).float() - ref_atom_name_chars = F.one_hot( - ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE - ).float() - # Bias-free downstream Linears require zeroed padding. - atm_mask_f = atm_mask.float() - ref_element = ref_element * atm_mask_f.unsqueeze(-1) - ref_atom_name_chars = ref_atom_name_chars * atm_mask_f.unsqueeze(-1).unsqueeze( - -1 - ) - - atom_to_token = atom_to_token * atm_mask.long() - - use_amp = ref_pos.device.type == "cuda" - with ( - torch.set_grad_enabled(res_type_soft is not None), - torch.amp.autocast("cuda", enabled=use_amp, dtype=torch.bfloat16), - ): - # 1. Input embeddings - x_inputs = self.inputs_embedder( - aatype=res_type_oh, - profile=profile.float(), - deletion_mean=deletion_mean.float(), - ref_pos=ref_pos, - atom_attention_mask=atm_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=atom_to_token, - ) - - # 2. Initialize pair representation - z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2( - x_inputs - ).unsqueeze(1) - - # 3. Positional encodings - relative_position_encoding = self.rel_pos( - residue_index=residue_index, - asym_id=asym_id, - sym_id=sym_id, - entity_id=entity_id, - token_index=token_index, - ) - token_bonds_encoding = self.token_bonds(token_bonds.float()) - z_init = z_init + relative_position_encoding + token_bonds_encoding - - # 4. Language model integration - if ( - lm_hidden_states is None - and input_ids is not None - and self._esmc is not None - ): - lm_hidden_states = self._compute_lm_hidden_states( - input_ids, asym_id, residue_index, mol_type, tok_mask - ) - if lm_hidden_states is not None: - lm_z = self.language_model( - lm_hidden_states.detach(), lm_dropout=self.config.lm_dropout - ) - z_init = z_init + lm_z.to(z_init.dtype) - - # MSA tensors prepared once: encoder consumes [B, L, M, ...] layout. - _msa_kwargs: dict | None = None - if self.msa_encoder is not None and msa is not None: - if msa.dim() == 4: - B_msa, M, L_msa, _ = msa.shape - msa_oh = msa.permute(0, 2, 1, 3).float() - else: - B_msa, M, L_msa = msa.shape - msa_oh = F.one_hot( - msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES - ).float() # [B, L, M, 33] - msa_attn = ( - msa_attention_mask.permute(0, 2, 1).float() - if msa_attention_mask is not None - else tok_mask[:, :, None].expand(-1, -1, M).float() - ) - # Bias-free MSAEncoder.embed requires zeroed padding. - msa_oh = msa_oh * msa_attn.unsqueeze(-1) - hd = ( - has_deletion.permute(0, 2, 1).float() - if has_deletion is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - dv = ( - deletion_value.permute(0, 2, 1).float() - if deletion_value is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - _msa_kwargs = dict( - x_inputs=x_inputs, - msa_oh=msa_oh, - has_deletion=hd, - deletion_value=dv, - msa_attention_mask=msa_attn, - ) - - # Expand 1-D token mask → 2-D pair mask for folding trunk - pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() - - # Loop: pair-only folding trunk, MSA encoder runs inside each iteration. - z = torch.zeros_like(z_init) - prev_pair: Tensor | None = None - prev_disto_probs: Tensor | None = None - for loop_num in range(n_loops + 1): - z = z_init + self.pair_loop_proj(z) - if _msa_kwargs is not None and self.msa_encoder is not None: - z = z + self.msa_encoder(x_pair=z, **_msa_kwargs).to(z.dtype) - z = self.folding_trunk(z, pair_attention_mask=pair_mask) - - # Loop early-exit - if early_exit and loop_num < n_loops: - l2_converged = False - if prev_pair is not None and loop_num > 0: - rel_l2 = ( - z.float() - prev_pair.float() - ).norm() / prev_pair.float().norm().clamp(min=1e-8) - l2_converged = rel_l2.item() < 0.25 - prev_pair = z.detach().clone() - - sym_z = z.float() + z.float().transpose(-2, -3) - cur_probs = F.softmax(self.distogram_head(sym_z).float(), dim=-1) - if prev_disto_probs is not None and loop_num > 0: - kl_per_pair = ( - cur_probs - * ( - cur_probs.clamp(min=1e-8) - / prev_disto_probs.clamp(min=1e-8) - ).log() - ).sum(-1) - kl = (kl_per_pair + kl_per_pair.transpose(-1, -2)).mean() / 2 - if l2_converged or kl.item() < 0.05: - break - prev_disto_probs = cur_probs.detach() - - # 6. Distogram (inside the trunk autocast so z stays bf16) - distogram_logits = self.distogram_head(z + z.transpose(-2, -3)) - - # 7. Diffusion sampling (always no_grad; optional seed for parity) - with torch.no_grad(), _seed_context(seed): - structure_output = self.structure_head.sample( - z_trunk=z.float(), - s_inputs=x_inputs, - s_trunk=None, - relative_position_encoding=relative_position_encoding, - ref_pos=ref_pos, - ref_charge=ref_charge, - ref_mask=atm_mask, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - ref_space_uid=ref_space_uid, - tok_idx=atom_to_token, - asym_id=asym_id, - residue_index=residue_index, - entity_id=entity_id, - token_index=token_index, - sym_id=sym_id, - token_attention_mask=tok_mask, - num_diffusion_samples=n_samples, - num_sampling_steps=num_sampling_steps, - return_atom_repr=False, - denoising_early_exit_rmsd=(0.10 if early_exit else None), - ) - - sample_coords = structure_output["sample_atom_coords"] - assert sample_coords is not None - output: dict[str, Tensor] = {"distogram_logits": distogram_logits} - output["sample_atom_coords"] = sample_coords - - if self.confidence_head is not None: - confidence_output = self.confidence_head( - s_inputs=x_inputs.detach(), - z=z.detach().float(), - x_pred=sample_coords.detach(), - distogram_atom_idx=disto_idx, - token_attention_mask=tok_mask, - atom_to_token=atom_to_token, - atom_attention_mask=atm_mask, - asym_id=asym_id, - mol_type=mol_type, - num_diffusion_samples=n_samples, - relative_position_encoding=relative_position_encoding.detach(), - token_bonds_encoding=token_bonds_encoding.detach(), - ) - output.update(confidence_output) - - # Pass-through tensors used by output decoders. - output["atom_pad_mask"] = ( - atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask - ) - output["residue_index"] = residue_index - output["entity_id"] = entity_id - - return output - - -__all__ = ["ESMFold2ExperimentalModel"] diff --git a/src/transformers/models/esmfold2/protein_utils.py b/src/transformers/models/esmfold2/protein_utils.py index a94c05c9c575..28aadf01e5c3 100644 --- a/src/transformers/models/esmfold2/protein_utils.py +++ b/src/transformers/models/esmfold2/protein_utils.py @@ -371,7 +371,7 @@ def _encode_atom_name(name: str) -> list[int]: def prepare_protein_features(sequence: str) -> dict[str, Tensor]: - """Featurize a single protein sequence for ESMFold2ExperimentalModel.forward. + """Featurize a single protein sequence for ESMFold2Model.forward. Returns the same keys with the same dtypes/shapes as ``ESMFold2InputBuilder.prepare_input(StructurePredictionInput(...))`` From 5e656aba715fbf7abbe89ece0751791d67052202 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 16:16:30 +0100 Subject: [PATCH 17/70] Remove TransformerEngine fp8 path from ESMFold2 ESMFold2's only TransformerEngine dependency was the optional fp8 quantization of the (now TE-free) ESMC backbone. Drop the import guard, the dead _convert_te_modules_to_fp8_inplace walker, the "fp8" precision option, and the fp8 padding/autocast plumbing. _lm_precision_context is now a plain bf16 autocast; ESMC loads at bf16 (default) or fp32. No other Transformers model depends on transformer_engine. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/modeling_esmfold2.py | 175 +----------------- .../esmfold2/modeling_esmfold2_common.py | 3 +- 2 files changed, 8 insertions(+), 170 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 530ecbc361ac..910e3acc341f 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -20,20 +20,6 @@ import torch.nn.functional as F from torch import Tensor -try: - import transformer_engine.pytorch as te # type: ignore[import] - from transformer_engine.common.recipe import ( # type: ignore[import] - DelayedScaling, - Format, - ) - - TE_AVAILABLE = True -except ImportError: - te = None # type: ignore[assignment] - DelayedScaling = None # type: ignore[assignment] - Format = None # type: ignore[assignment] - TE_AVAILABLE = False - from ...modeling_utils import PreTrainedModel # type: ignore[import] from .configuration_esmfold2 import ESMFold2Config from .modeling_esmfold2_common import ( @@ -57,6 +43,7 @@ gather_token_to_atom, ) + _EPS = 1e-6 _NONPOLYMER_ID = 4 @@ -349,139 +336,11 @@ def _inverse_softplus(value: float) -> float: return value + math.log(-math.expm1(-value)) -def _convert_te_modules_to_fp8_inplace(module: nn.Module) -> None: - """Re-init each TE module via quantized_model_init so weights live as fp8. - - Must be called inside torch.no_grad(); covers nn.Linear, te.Linear, - te.LayerNormLinear, te.LayerNormMLP — the last two hold 99% of ESMC weight. - """ - if not TE_AVAILABLE: - raise RuntimeError("transformer_engine is not available; cannot use fp8.") - from transformer_engine.pytorch import quantized_model_init # type: ignore[import] - - def _walk(mod: nn.Module) -> None: - for name, child in list(mod.named_children()): - replaced = False - if isinstance(child, nn.Linear): - in_f, out_f = child.in_features, child.out_features - has_bias = child.bias is not None - device = child.weight.device - dtype = child.weight.dtype - w = child.weight.data - b = child.bias.data if has_bias else None - setattr(mod, name, nn.Identity()) - del child - torch.cuda.empty_cache() - with quantized_model_init(enabled=True): - new_mod = te.Linear( # type: ignore[union-attr] - in_f, out_f, bias=has_bias, params_dtype=dtype - ).to(device) - new_mod.weight.quantize_(w) # type: ignore[attr-defined,operator] - if has_bias: - assert b is not None - new_mod.bias.data.copy_(b) # type: ignore[union-attr] - del w, b - replaced = True - elif isinstance(child, te.Linear): # type: ignore[union-attr] - # te.Linear with bf16 weight → re-init inside quantized_model_init for fp8. - in_f, out_f = child.in_features, child.out_features - has_bias = child.bias is not None - device = child.weight.device - dtype = ( - child.weight.dtype - if not hasattr(child.weight, "_data") - else torch.bfloat16 - ) - state = {k: v.detach().clone() for k, v in child.state_dict().items()} - setattr(mod, name, nn.Identity()) - del child - torch.cuda.empty_cache() - with quantized_model_init(enabled=True): - new_mod = te.Linear( # type: ignore[union-attr] - in_f, - out_f, - bias=has_bias, - params_dtype=dtype, # type: ignore[arg-type] - ).to(device) # type: ignore[arg-type] - new_mod.load_state_dict(state, strict=False) - replaced = True - elif ( - hasattr(te, "LayerNormLinear") and isinstance(child, te.LayerNormLinear) # type: ignore[union-attr] - ): - state = {k: v.detach().clone() for k, v in child.state_dict().items()} - hidden_size = child.in_features - out_features = child.out_features - has_bias = child.use_bias - device = next(child.parameters()).device - setattr(mod, name, nn.Identity()) - del child - torch.cuda.empty_cache() - with quantized_model_init(enabled=True): - new_mod = te.LayerNormLinear( # type: ignore[union-attr] - hidden_size, - out_features, - bias=has_bias, - params_dtype=torch.bfloat16, - ).to(device) - new_mod.load_state_dict(state, strict=False) - replaced = True - elif ( - hasattr(te, "LayerNormMLP") and isinstance(child, te.LayerNormMLP) # type: ignore[union-attr] - ): - state = {k: v.detach().clone() for k, v in child.state_dict().items()} - fc1_weight: Tensor = child.fc1_weight # type: ignore[attr-defined] - hidden_size = int(fc1_weight.shape[1]) - # fc1 packed as (2*ffn_hidden_size, hidden_size) for swiglu. - ffn_hidden_size = int(fc1_weight.shape[0]) // 2 - has_bias = ( - getattr(child, "fc1_bias", None) is not None - and child.fc1_bias is not None # type: ignore[attr-defined] - ) - device = fc1_weight.device - setattr(mod, name, nn.Identity()) - del child - torch.cuda.empty_cache() - with quantized_model_init(enabled=True): - new_mod = te.LayerNormMLP( # type: ignore[union-attr] - hidden_size=hidden_size, - ffn_hidden_size=ffn_hidden_size, - bias=has_bias, - activation="swiglu", - params_dtype=torch.bfloat16, - ).to(device) # type: ignore[arg-type] - new_mod.load_state_dict(state, strict=False) - replaced = True - - if replaced: - # Freeze via .eval()+.requires_grad_(False); per-param ops would unwrap Float8Tensor. - new_mod.eval().requires_grad_(False) - setattr(mod, name, new_mod) - torch.cuda.empty_cache() - else: - _walk(child) - - _walk(module) - torch.cuda.empty_cache() - - @contextmanager -def _lm_precision_context(fp8: bool): - """bf16 autocast (+ optional TE fp8 autocast) around the LM forward. - - te.autocast keeps te.Linear outputs bf16 instead of the fp32 default - (~425 MB at L=1024 in the hidden-state cache). - """ +def _lm_precision_context(): + """bf16 autocast around the LM (ESMC backbone) forward.""" with torch.autocast(device_type="cuda", dtype=torch.bfloat16): - if fp8 and TE_AVAILABLE: - fp8_recipe = DelayedScaling( # type: ignore[misc] - fp8_format=Format.HYBRID, # type: ignore[union-attr] - amax_history_len=1, - amax_compute_algo="most_recent", - ) - with te.autocast(enabled=True, recipe=fp8_recipe): # type: ignore[union-attr] - yield - else: - yield + yield class ESMFold2Model(PreTrainedModel): @@ -528,7 +387,6 @@ def __init__(self, config: ESMFold2Config) -> None: d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers ) self._esmc: nn.Module | None = None - self._esmc_fp8: bool = False # set by load_esmc(fp8=True) pf = config.folding_trunk self.folding_trunk = FoldingTrunk( @@ -580,12 +438,7 @@ def __init__(self, config: ESMFold2Config) -> None: self.post_init() def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: - """Load the ESMC LM. - - ``precision``: ``"bf16"`` (default), ``"fp32"``, or ``"fp8"``. - ``"fp8"`` requires H100 + TransformerEngine ≥ 2.x and quantizes - every TE module's weights to fp8 storage. - """ + """Load the ESMC LM backbone. ``precision``: ``"bf16"`` (default) or ``"fp32"``.""" # Resolve the ESMC backbone through the Auto registry (model_type "esmc" # -> ESMCModel) rather than a hard cross-model import. ESMC is a shared, # frozen backbone loaded from its own repo (`esmc_id`), not bundled here. @@ -594,7 +447,6 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: dtype_map = { "bf16": torch.bfloat16, "fp32": torch.float32, - "fp8": torch.bfloat16, # underlying weights stay bf16, TE re-quantizes to fp8 } if precision not in dtype_map: raise ValueError( @@ -610,17 +462,6 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: for p in esmc.parameters(): p.requires_grad_(False) - if precision == "fp8": - if not TE_AVAILABLE: - raise RuntimeError( - "transformer_engine is not available; cannot use fp8." - ) - with torch.no_grad(): - _convert_te_modules_to_fp8_inplace(esmc) - self._esmc_fp8 = True - else: - self._esmc_fp8 = False - self._esmc = esmc @classmethod @@ -691,9 +532,7 @@ def _compute_lm_hidden_states( tok_mask: Tensor, ) -> Tensor: assert self._esmc is not None - # fp8 TE kernels require prod(shape[:-1]) % 8 == 0. - pad_to = 8 if self._esmc_fp8 else None - with _lm_precision_context(self._esmc_fp8): + with _lm_precision_context(): return compute_lm_hidden_states( self._esmc, input_ids, @@ -701,7 +540,7 @@ def _compute_lm_hidden_states( residue_index, mol_type, tok_mask, - pad_to_multiple=pad_to, + pad_to_multiple=None, ) def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 7a6ae88912b3..0de868d3f15e 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -2062,8 +2062,7 @@ def compute_lm_hidden_states( expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] expand_maps.append(expand_map) - # Pad to longest LM input; round to ``pad_to_multiple`` when fp8 is on - # (TE fp8 kernels assert prod(shape[:-1]) % 8 == 0). + # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. max_len = max(lm_lengths) if pad_to_multiple is not None and pad_to_multiple > 1: max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple From 7676ea17e709ab760b152e9b8a3e04ffb3eeab34 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 16:30:41 +0100 Subject: [PATCH 18/70] Route ESMFold2 plain self-attention through the v5 attention interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SWA3DRoPEAttention's plain softmax(QKᵀ)V core now dispatches through ALL_ATTENTION_FUNCTIONS / a local eager_attention_forward, keyed on config._attn_implementation, with the sliding window expressed as an additive attention mask. The custom flash-attention path (native bidirectional window_size + varlen for packed inputs) is kept as an opt-in backend, now gated on _attn_implementation == "flash_attention_2" instead of auto-selecting whenever flash-attn is importable — so the default is sdpa (matching the fork's SDPA fallback bit-for-bit) and flash is opt-in, per v5 conventions. ESMFold2Model declares _supports_sdpa / _supports_flash_attn / _supports_attention_backend and, after construction, attaches its shared config to every SWA3DRoPEAttention (the atom encoders/decoders build them from explicit dims), so dispatch stays live under set_attn_implementation. is_causal=False guards against the interface defaulting to causal when no mask is passed. Pair-bias (AttentionPairBias) and triangular math are left untouched. Validated on CPU vs the pre-refactor forward (random weights): sdpa max|Δ|=0.0 (bit-exact), eager max|Δ|=1.3e-3 (bf16 softmax precision). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/modeling_esmfold2.py | 13 +++ .../esmfold2/modeling_esmfold2_common.py | 80 +++++++++++++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 910e3acc341f..56e365f3bfd5 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -34,6 +34,7 @@ OuterProductMean, ResIdxAsymIdSymIdEntityIdEncoding, RowAttentionPooling, + SWA3DRoPEAttention, SwiGLUMLP, TriangleMultiplicativeUpdate, _categorical_mean, @@ -368,6 +369,9 @@ class ESMFold2Model(PreTrainedModel): config_class = ESMFold2Config _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + _supports_sdpa = True + _supports_flash_attn = True + _supports_attention_backend = True def __init__(self, config: ESMFold2Config) -> None: super().__init__(config) @@ -435,6 +439,15 @@ def __init__(self, config: ESMFold2Config) -> None: msa_head_width=msa_cfg.msa_head_width, ) + # SWA3DRoPEAttention modules live deep in the atom encoders/decoders and + # are built from explicit dims, so give each a handle to the model config: + # their forward dispatches the plain-attention core through the v5 + # attention interface (config._attn_implementation), staying live under + # set_attn_implementation() since the config object is shared. + for module in self.modules(): + if isinstance(module, SWA3DRoPEAttention): + module.config = self.config + self.post_init() def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 0de868d3f15e..28a1ceeb8c8f 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -11,6 +11,7 @@ from __future__ import annotations import random +from collections.abc import Callable from contextlib import contextmanager from functools import partial from typing import cast @@ -22,6 +23,7 @@ from torch import Tensor from torch.utils.checkpoint import checkpoint + try: from flash_attn import ( # type: ignore[import] flash_attn_func, @@ -40,6 +42,7 @@ pad_input = None # type: ignore[assignment] FLASH_ATTN_AVAILABLE = False +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS # type: ignore[import] from .configuration_esmfold2 import ESMFold2Config @@ -420,15 +423,60 @@ def forward(self, x: Tensor) -> Tensor: # =========================================================================== +def eager_attention_forward( + module: nn.Module, + query: Tensor, + key: Tensor, + value: Tensor, + attention_mask: Tensor | None, + scaling: float, + dropout: float = 0.0, + **kwargs, +) -> tuple[Tensor, Tensor]: + """Reference attention used as the eager backend / fallback for the v5 + attention interface. Inputs/outputs follow the ``ALL_ATTENTION_FUNCTIONS`` + convention: ``query``/``key``/``value`` are ``[B, H, S, Dh]`` and the + returned context is ``[B, S, H, Dh]``. ESMFold2 has no grouped-query + attention, so there is no ``repeat_kv``.""" + attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling + if attention_mask is not None: + attn_weights = attn_weights + attention_mask + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( + query.dtype + ) + attn_weights = nn.functional.dropout( + attn_weights, p=dropout, training=module.training + ) + attn_output = torch.matmul(attn_weights, value) + attn_output = attn_output.transpose(1, 2).contiguous() + return attn_output, attn_weights + + class SWA3DRoPEAttention(nn.Module): - """Sliding window attention with 3D RoPE. Has Wqkv, gate_proj, out_proj.""" + """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. + + The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention + interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), + with the sliding window expressed as an additive attention mask. The custom + flash-attention path (native bidirectional ``window_size``, plus varlen for + packed inputs) is kept as an opt-in backend, selected when + ``_attn_implementation == "flash_attention_2"``. ``config`` is attached by + the parent ``ESMFold2Model`` after construction; it is ``None`` (→ ``sdpa``) + when the module is used standalone. + """ def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: super().__init__() + self.config = None self.n_heads = n_heads self.head_dim = d_model // n_heads self.scale = self.head_dim**-0.5 self.half_window = half_window + # No grouped-query attention; identity repeat keeps the interface happy. + self.num_key_value_groups = 1 + # Bidirectional encoder: never let the sdpa/flash interface default to + # causal masking when attention_mask happens to be None. + self.is_causal = False self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) self.out_proj = nn.Linear(d_model, d_model, bias=False) @@ -451,7 +499,12 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: if q.dtype not in (torch.float16, torch.bfloat16): q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() - if len(attention_params) > 2 and FLASH_ATTN_AVAILABLE: + attn_impl = ( + self.config._attn_implementation if self.config is not None else "sdpa" + ) + use_flash = attn_impl == "flash_attention_2" and FLASH_ATTN_AVAILABLE + + if use_flash and len(attention_params) > 2: indices, cu_seqlens, max_seqlen = ( attention_params[2], attention_params[3], @@ -478,7 +531,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: window_size=(self.half_window, self.half_window), ) out = pad_input(out_unpad, indices, B, N) # type: ignore[misc] - elif FLASH_ATTN_AVAILABLE: + elif use_flash: out = flash_attn_func( # type: ignore[misc] q, k, @@ -497,13 +550,26 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) allowed |= torch.eye(N, dtype=torch.bool, device=q.device) - out = F.scaled_dot_product_attention( + # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. + attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) + attn_mask = attn_mask.masked_fill( + ~allowed.unsqueeze(1), torch.finfo(q.dtype).min + ) + + attention_interface: Callable = eager_attention_forward + if attn_impl != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( + attn_impl, eager_attention_forward + ) + out, _ = attention_interface( + self, q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), - attn_mask=allowed.unsqueeze(1), - scale=self.scale, - ).transpose(1, 2) + attn_mask, + dropout=0.0, + scaling=self.scale, + ) out = out * valid.unsqueeze(-1).unsqueeze(-1) out = out.to(input_dtype).reshape(B, N, -1) # type: ignore[union-attr] From 0a2219202562a01aa95fe97449654ebaa1082483 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 17:07:50 +0100 Subject: [PATCH 19/70] Convert ESMFold2Config to the v5 @strict / PreTrainedConfig style The config was one of ~4 holdouts still using the old `def __init__(**kwargs)` + hand-rolled `to_dict` style; 460/463 configs use `@strict` dataclass `PreTrainedConfig`. Rewrite it to match, mirroring `models/esm` (ESMFold v1): - Every sub-config (MSAEncoder/Parcae/LMEncoder/AtomAttention/FoldingTrunk/ InputsEmbedder/DiffusionModule/DiffusionStructureHead/ConfidenceHead) is now a `@strict PreTrainedConfig` with typed fields + defaults, instead of a plain `@dataclass`. - Nesting is declared via `sub_configs = {...}` on each parent + a `__post_init__` that turns `dict -> SubConfig(**dict)`. This deletes the brittle hand-rolled `ESMFold2Config.to_dict()` (base class now serializes via `sub_configs`) and gives recursive `_attn_implementation` propagation to sub-configs for free. - Top config switches to `@auto_docstring(checkpoint="biohub/ESMFold2")`, which resolves the outstanding `check_docstrings` failure for ESMFold2Config. - `__all__` is reduced to `["ESMFold2Config"]` (sub-configs are implementation detail, matching ESM which exports only `EsmConfig`). check_config_attributes: top config keeps a precise 4-item allow-list (type + three training/experimental recipe knobs not read by the core inference path); the 9 sub-configs are allow-listed wholesale because their fields are threaded into submodules as explicit dims (e.g. `d_atom=cfg.inputs.atom_encoder.d_atom`), which the checker's `config.` heuristic cannot trace. Verified: default + nested-dict construction, save/load round-trip (to_dict identical, sub-config types preserved), recursive attn-impl propagation, type validation, unknown-kwarg tolerance; the tiny ESMFold2Model still builds and the SWA attention equivalence is unchanged. ruff + check_docstrings + check_config_attributes all clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/configuration_esmfold2.py | 431 +++++++++--------- utils/check_config_attributes.py | 21 + 2 files changed, 243 insertions(+), 209 deletions(-) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 4f0548dc10aa..539a5b906b71 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -13,151 +13,167 @@ # limitations under the License. """ESMFold2 model configuration.""" -from __future__ import annotations +from huggingface_hub.dataclasses import strict -from dataclasses import asdict, dataclass, field +from ...configuration_utils import PreTrainedConfig +from ...utils import auto_docstring, logging -from ...configuration_utils import PretrainedConfig # type: ignore[import] -# --------------------------------------------------------------------------- -# Nested dataclass configs -# --------------------------------------------------------------------------- +logger = logging.get_logger(__name__) _DEFAULT_ESMC_HF_REPO = "biohub/ESMC-6B" -@dataclass -class MSAEncoderConfig: +# --------------------------------------------------------------------------- +# Nested sub-configs (registered via the parent's ``sub_configs``) +# --------------------------------------------------------------------------- + + +@strict +class MSAEncoderConfig(PreTrainedConfig): """Config for the optional MSA encoder module (Large MSA models only).""" - enabled: bool = False - d_msa: int = 128 - d_hidden: int = 32 - n_layers: int = 4 - n_heads_msa: int = 8 - msa_head_width: int = 32 + enabled: bool | None = False + d_msa: int | None = 128 + d_hidden: int | None = 32 + n_layers: int | None = 4 + n_heads_msa: int | None = 8 + msa_head_width: int | None = 32 -@dataclass -class ParcaeConfig: +@strict +class ParcaeConfig(PreTrainedConfig): """Release-only config for the parcae diffusion-loop scheduler.""" - enabled: bool = True - poisson_mean: float = 3.0 - min_steps: int = 1 + enabled: bool | None = True + poisson_mean: float | None = 3.0 + min_steps: int | None = 1 max_steps: int | None = 6 - coda_n_layers: int = 2 + coda_n_layers: int | None = 2 -@dataclass -class LMEncoderConfig: +@strict +class LMEncoderConfig(PreTrainedConfig): """Release-only config for the LM-side pair encoder.""" - enabled: bool = True - n_layers: int = 4 - lm_dropout: float = 0.25 - per_loop_lm_dropout: bool = True + enabled: bool | None = True + n_layers: int | None = 4 + lm_dropout: float | None = 0.25 + per_loop_lm_dropout: bool | None = True -@dataclass -class AtomAttentionConfig: - """Config for SWA atom encoder/decoder with 3D RoPE.""" +@strict +class AtomAttentionConfig(PreTrainedConfig): + """Config for the SWA atom encoder/decoder with 3D RoPE.""" - d_atom: int = 128 - d_token: int = 768 - n_blocks: int = 3 - n_heads: int = 4 - swa_window_size: int = 128 - expansion_ratio: int = 2 - # 3D RoPE config - spatial_rope_base_frequency: float = 20.0 - n_spatial_rope_pairs_per_axis: int = 2 - n_uid_rope_pairs: int = 10 - uid_rope_base_frequency: float = 10000.0 + d_atom: int | None = 128 + d_token: int | None = 768 + n_blocks: int | None = 3 + n_heads: int | None = 4 + swa_window_size: int | None = 128 + expansion_ratio: int | None = 2 + spatial_rope_base_frequency: float | None = 20.0 + n_spatial_rope_pairs_per_axis: int | None = 2 + n_uid_rope_pairs: int | None = 10 + uid_rope_base_frequency: float | None = 10000.0 -@dataclass -class FoldingTrunkConfig: - n_layers: int = 24 - n_heads: int = 8 - dropout: float = 0.0 +@strict +class FoldingTrunkConfig(PreTrainedConfig): + """Config for a pairwise folding trunk stack.""" + n_layers: int | None = 24 + n_heads: int | None = 8 + dropout: float | None = 0.0 -@dataclass -class InputsEmbedderConfig: - d_inputs: int = 451 - atom_encoder: AtomAttentionConfig = field(default_factory=AtomAttentionConfig) - def __post_init__(self): - if isinstance(self.atom_encoder, dict): +@strict +class InputsEmbedderConfig(PreTrainedConfig): + """Config for the inputs embedder (wraps the atom encoder).""" + + sub_configs = {"atom_encoder": AtomAttentionConfig} + + d_inputs: int | None = 451 + atom_encoder: dict | AtomAttentionConfig | None = None + + def __post_init__(self, **kwargs): + if self.atom_encoder is None: + self.atom_encoder = AtomAttentionConfig() + elif isinstance(self.atom_encoder, dict): self.atom_encoder = AtomAttentionConfig(**self.atom_encoder) + super().__post_init__(**kwargs) -@dataclass -class DiffusionModuleConfig: +@strict +class DiffusionModuleConfig(PreTrainedConfig): """Config for the DiffusionModule.""" - sigma_data: float = 16.0 - c_atom: int = 128 - c_token: int = 768 - c_z: int = 256 - c_s_inputs: int = 451 - fourier_dim: int = 256 - relpos_r_max: int = 32 - relpos_s_max: int = 2 - atom_num_blocks: int = 3 - atom_num_heads: int = 4 - token_num_blocks: int = 12 - token_num_heads: int = 16 - transition_multiplier: int = 2 - - -@dataclass -class DiffusionStructureHeadConfig: + sigma_data: float | None = 16.0 + c_atom: int | None = 128 + c_token: int | None = 768 + c_z: int | None = 256 + c_s_inputs: int | None = 451 + fourier_dim: int | None = 256 + relpos_r_max: int | None = 32 + relpos_s_max: int | None = 2 + atom_num_blocks: int | None = 3 + atom_num_heads: int | None = 4 + token_num_blocks: int | None = 12 + token_num_heads: int | None = 16 + transition_multiplier: int | None = 2 + + +@strict +class DiffusionStructureHeadConfig(PreTrainedConfig): """Config for the diffusion-based structure prediction head.""" - diffusion_module: DiffusionModuleConfig = field( - default_factory=DiffusionModuleConfig - ) - distogram_bins: int = 128 + sub_configs = {"diffusion_module": DiffusionModuleConfig} + diffusion_module: dict | DiffusionModuleConfig | None = None + distogram_bins: int | None = 128 # Training noise: sigma ~ sigma_data * exp(mu + sigma * N(0,1)) - train_noise_log_mean: float = -1.2 - train_noise_log_std: float = 1.5 - + train_noise_log_mean: float | None = -1.2 + train_noise_log_std: float | None = 1.5 # Sampling defaults (ODE) - gamma_0: float = 0.605 - gamma_min: float = 1.107 - noise_scale: float = 0.0 - step_scale: float = 1.0 - + gamma_0: float | None = 0.605 + gamma_min: float | None = 1.107 + noise_scale: float | None = 0.0 + step_scale: float | None = 1.0 # Inference schedule defaults - inference_s_max: float = 160.0 - inference_s_min: float = 4e-4 - inference_p: float = 8.0 - inference_num_steps: int = 68 - - def __post_init__(self): - if isinstance(self.diffusion_module, dict): + inference_s_max: float | None = 160.0 + inference_s_min: float | None = 4e-4 + inference_p: float | None = 8.0 + inference_num_steps: int | None = 68 + + def __post_init__(self, **kwargs): + if self.diffusion_module is None: + self.diffusion_module = DiffusionModuleConfig() + elif isinstance(self.diffusion_module, dict): self.diffusion_module = DiffusionModuleConfig(**self.diffusion_module) + super().__post_init__(**kwargs) -@dataclass -class ConfidenceHeadConfig: - enabled: bool = True - num_plddt_bins: int = 50 - num_pde_bins: int = 64 - num_pae_bins: int = 64 - min_dist: float = 2.0 - max_dist: float = 52.0 - distogram_bins: int = 128 - folding_trunk: FoldingTrunkConfig = field( - default_factory=lambda: FoldingTrunkConfig(n_layers=4) - ) - - def __post_init__(self): - if isinstance(self.folding_trunk, dict): +@strict +class ConfidenceHeadConfig(PreTrainedConfig): + """Config for the confidence prediction head.""" + + sub_configs = {"folding_trunk": FoldingTrunkConfig} + + enabled: bool | None = True + num_plddt_bins: int | None = 50 + num_pde_bins: int | None = 64 + num_pae_bins: int | None = 64 + min_dist: float | None = 2.0 + max_dist: float | None = 52.0 + distogram_bins: int | None = 128 + folding_trunk: dict | FoldingTrunkConfig | None = None + + def __post_init__(self, **kwargs): + if self.folding_trunk is None: + self.folding_trunk = FoldingTrunkConfig(n_layers=4) + elif isinstance(self.folding_trunk, dict): self.folding_trunk = FoldingTrunkConfig(**self.folding_trunk) + super().__post_init__(**kwargs) # --------------------------------------------------------------------------- @@ -165,48 +181,58 @@ def __post_init__(self): # --------------------------------------------------------------------------- -class ESMFold2Config(PretrainedConfig): - """ - Configuration for the ESMFold2 structure prediction model. - - Uses SWA atom encoders with 3D RoPE, a diffusion transformer, - a folding trunk, and an ESMC 6B PLM backbone. - - Configuration objects inherit from [`PretrainedConfig`] and can be used to control - the model outputs. Read the documentation from [`PretrainedConfig`] for more - information. - - Args: - d_single (`int`, defaults to 384): - Dimensionality of single (per-residue) representations. - d_pair (`int`, defaults to 256): - Dimensionality of pair (residue-residue) representations. - n_relative_residx_bins (`int`, defaults to 32): - Number of bins for relative residue index encoding. - n_relative_chain_bins (`int`, defaults to 2): - Number of bins for relative chain encoding. - num_loops (`int`, defaults to 10): - Number of trunk loops for iterative refinement. - num_diffusion_samples (`int`, defaults to 8): - Number of parallel structure predictions to generate. - lm_dropout (`float`, defaults to 0.0): - Dropout probability on LM pair embeddings. When > 0, dropout is - applied with ``training=True`` (including at inference) to match - the experimental training recipe used by binder design. - force_lm_dropout_during_inference (`bool`, defaults to False): - When True, apply ``lm_dropout`` even when ``model.eval()`` and - ``lm_dropout`` > 0. Binder-design loads set this to True. - disable_msa_features (`bool`, defaults to False): - When True, zero out MSA-derived ``profile`` and ``deletion_mean`` - before the inputs embedder (experimental medium/large checkpoints). - inputs (`InputsEmbedderConfig`): - Configuration for the inputs embedder module. - folding_trunk (`FoldingTrunkConfig`): - Configuration for the folding trunk. - structure_head (`DiffusionStructureHeadConfig`): - Configuration for the diffusion-based structure prediction head. - confidence_head (`ConfidenceHeadConfig`): - Configuration for the confidence prediction head. +@auto_docstring(checkpoint="biohub/ESMFold2") +@strict +class ESMFold2Config(PreTrainedConfig): + r""" + type (`str`, *optional*, defaults to `"release"`): + Architecture variant. Only `"release"` is supported in this port (the + `"experimental"` variant is deferred to a follow-up). + d_single (`int`, *optional*, defaults to 384): + Dimensionality of single (per-residue) representations. + d_pair (`int`, *optional*, defaults to 256): + Dimensionality of pair (residue-residue) representations. + n_relative_residx_bins (`int`, *optional*, defaults to 32): + Number of bins for relative residue index encoding. + n_relative_chain_bins (`int`, *optional*, defaults to 2): + Number of bins for relative chain encoding. + num_loops (`int`, *optional*, defaults to 10): + Number of trunk loops for iterative refinement. + num_diffusion_samples (`int`, *optional*, defaults to 8): + Number of parallel structure predictions to generate. + disable_msa_features (`bool`, *optional*, defaults to `False`): + When `True`, zero out MSA-derived `profile` and `deletion_mean` before + the inputs embedder. + lm_dropout (`float`, *optional*, defaults to 0.0): + Dropout probability on LM pair embeddings. When > 0, dropout is applied + with `training=True` (including at inference) to match the binder-design + training recipe. + force_lm_dropout_during_inference (`bool`, *optional*, defaults to `False`): + When `True`, apply `lm_dropout` even under `model.eval()`. + lm_d_model (`int`, *optional*, defaults to 2560): + Hidden size of the ESMC language-model backbone. + lm_num_layers (`int`, *optional*, defaults to 80): + Number of layers in the ESMC language-model backbone. + esmc_id (`str`, *optional*, defaults to `"biohub/ESMC-6B"`): + Hub id of the ESMC backbone, loaded separately (the ESMFold2 checkpoint + does not bundle ESMC weights). + msa_encoder_overwrite (`bool`, *optional*, defaults to `True`): + If `True`, MSA encoder output replaces the pair stream; if `False`, it + is added. + inputs (`InputsEmbedderConfig`, *optional*): + Configuration for the inputs embedder module. + folding_trunk (`FoldingTrunkConfig`, *optional*): + Configuration for the folding trunk. + structure_head (`DiffusionStructureHeadConfig`, *optional*): + Configuration for the diffusion-based structure prediction head. + confidence_head (`ConfidenceHeadConfig`, *optional*): + Configuration for the confidence prediction head. + msa_encoder (`MSAEncoderConfig`, *optional*): + Configuration for the optional MSA encoder. + parcae (`ParcaeConfig`, *optional*): + Configuration for the parcae diffusion-loop scheduler. + lm_encoder (`LMEncoderConfig`, *optional*): + Configuration for the LM-side pair encoder. Examples: @@ -214,7 +240,7 @@ class ESMFold2Config(PretrainedConfig): >>> from transformers import ESMFold2Config, ESMFold2Model >>> # Initializing an ESMFold2 configuration - >>> configuration = ESMFold2Config(type="release") + >>> configuration = ESMFold2Config() >>> # Initializing a model (with random weights) from the configuration >>> model = ESMFold2Model(configuration) @@ -225,74 +251,61 @@ class ESMFold2Config(PretrainedConfig): """ model_type = "esmfold2" - has_no_defaults_at_init = True - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.type: str = kwargs.get("type", "release") + sub_configs = { + "inputs": InputsEmbedderConfig, + "folding_trunk": FoldingTrunkConfig, + "structure_head": DiffusionStructureHeadConfig, + "confidence_head": ConfidenceHeadConfig, + "msa_encoder": MSAEncoderConfig, + "parcae": ParcaeConfig, + "lm_encoder": LMEncoderConfig, + } + + type: str | None = "release" + d_single: int | None = 384 + d_pair: int | None = 256 + n_relative_residx_bins: int | None = 32 + n_relative_chain_bins: int | None = 2 + num_loops: int | None = 10 + num_diffusion_samples: int | None = 8 + disable_msa_features: bool | None = False + lm_dropout: float | None = 0.0 + force_lm_dropout_during_inference: bool | None = False + lm_d_model: int | None = 2560 + lm_num_layers: int | None = 80 + esmc_id: str | None = _DEFAULT_ESMC_HF_REPO + msa_encoder_overwrite: bool | None = True + inputs: dict | InputsEmbedderConfig | None = None + folding_trunk: dict | FoldingTrunkConfig | None = None + structure_head: dict | DiffusionStructureHeadConfig | None = None + confidence_head: dict | ConfidenceHeadConfig | None = None + msa_encoder: dict | MSAEncoderConfig | None = None + parcae: dict | ParcaeConfig | None = None + lm_encoder: dict | LMEncoderConfig | None = None + + def __post_init__(self, **kwargs): if self.type != "release": raise ValueError( "ESMFold2Config.type must be 'release' (the 'experimental' variant " f"is not included in this release), got {self.type!r}" ) - # Top-level scalar fields - self.d_single: int = kwargs.get("d_single", 384) - self.d_pair: int = kwargs.get("d_pair", 256) - self.n_relative_residx_bins: int = kwargs.get("n_relative_residx_bins", 32) - self.n_relative_chain_bins: int = kwargs.get("n_relative_chain_bins", 2) - self.num_loops: int = kwargs.get("num_loops", 10) - self.num_diffusion_samples: int = kwargs.get("num_diffusion_samples", 8) - # If True, ``profile`` / ``deletion_mean`` are zeroed before the inputs - # embedder. - self.disable_msa_features: bool = kwargs.get("disable_msa_features", False) - self.lm_dropout: float = kwargs.get("lm_dropout", 0.0) - self.force_lm_dropout_during_inference: bool = kwargs.get( - "force_lm_dropout_during_inference", False - ) - - self.lm_d_model: int = kwargs.get("lm_d_model", 2560) - self.lm_num_layers: int = kwargs.get("lm_num_layers", 80) - # Required, no default — every shipped HF export must name its ESMC backbone. - self.esmc_id: str = kwargs.get("esmc_id", _DEFAULT_ESMC_HF_REPO) - def _init_nested(cls, val): - if isinstance(val, cls): - return val + if val is None: + return cls() if isinstance(val, dict): return cls(**val) - return cls() - - self.inputs = _init_nested(InputsEmbedderConfig, kwargs.get("inputs")) - self.folding_trunk = _init_nested( - FoldingTrunkConfig, kwargs.get("folding_trunk") - ) - self.structure_head = _init_nested( - DiffusionStructureHeadConfig, kwargs.get("structure_head") - ) - self.confidence_head = _init_nested( - ConfidenceHeadConfig, kwargs.get("confidence_head") - ) - self.msa_encoder = _init_nested(MSAEncoderConfig, kwargs.get("msa_encoder")) - # Release-only modules — ignored when ``type == "experimental"``. - self.parcae = _init_nested(ParcaeConfig, kwargs.get("parcae")) - self.lm_encoder = _init_nested(LMEncoderConfig, kwargs.get("lm_encoder")) - # If True, MSA encoder output replaces the pair stream; if False, it is added. - self.msa_encoder_overwrite: bool = bool( - kwargs.get("msa_encoder_overwrite", True) - ) - - def to_dict(self): - output = super().to_dict() - output["inputs"] = asdict(self.inputs) - output["folding_trunk"] = asdict(self.folding_trunk) - output["structure_head"] = asdict(self.structure_head) - output["confidence_head"] = asdict(self.confidence_head) - output["msa_encoder"] = asdict(self.msa_encoder) - output["parcae"] = asdict(self.parcae) - output["lm_encoder"] = asdict(self.lm_encoder) - return output - - -__all__ = ["ESMFold2Config", "MSAEncoderConfig", "ParcaeConfig", "LMEncoderConfig"] + return val + + self.inputs = _init_nested(InputsEmbedderConfig, self.inputs) + self.folding_trunk = _init_nested(FoldingTrunkConfig, self.folding_trunk) + self.structure_head = _init_nested(DiffusionStructureHeadConfig, self.structure_head) + self.confidence_head = _init_nested(ConfidenceHeadConfig, self.confidence_head) + self.msa_encoder = _init_nested(MSAEncoderConfig, self.msa_encoder) + self.parcae = _init_nested(ParcaeConfig, self.parcae) + self.lm_encoder = _init_nested(LMEncoderConfig, self.lm_encoder) + + super().__post_init__(**kwargs) + + +__all__ = ["ESMFold2Config"] diff --git a/utils/check_config_attributes.py b/utils/check_config_attributes.py index 19ce5d0b3f8f..e273911eb8ca 100644 --- a/utils/check_config_attributes.py +++ b/utils/check_config_attributes.py @@ -146,6 +146,27 @@ "GlmMoeDsaConfig": ["head_dim", "layer_types", "mlp_bias", "first_k_dense_replace", "n_routed_experts"], "EsmFoldConfig": ["esm_ablate_pairwise", "esm_ablate_sequence", "esm_input_dropout", "esm_type"], "TrunkConfig": ["cpu_grad_checkpoint", "layer_drop"], + # type: architecture-variant marker (validated, "release"-only). The other + # three are training/experimental recipe knobs not consulted by the core + # single-sequence inference path (per-loop LM dropout reads lm_encoder.lm_dropout). + "ESMFold2Config": [ + "disable_msa_features", + "force_lm_dropout_during_inference", + "lm_dropout", + "type", + ], + # ESMFold2 sub-configs: their fields are threaded into submodules as explicit + # dims (e.g. ESMFold2AtomEncoder(d_atom=cfg.inputs.atom_encoder.d_atom, ...)), + # never read as `config.`, so the checker's heuristic can't trace them. + "AtomAttentionConfig": True, + "ConfidenceHeadConfig": True, + "DiffusionModuleConfig": True, + "DiffusionStructureHeadConfig": True, + "FoldingTrunkConfig": True, + "InputsEmbedderConfig": True, + "LMEncoderConfig": True, + "MSAEncoderConfig": True, + "ParcaeConfig": True, "SeamlessM4TConfig": True, "SeamlessM4Tv2Config": True, "ConditionalDetrConfig": True, From e4ea09049327a8a8a40e254970b929af591d15bf Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 18:20:59 +0100 Subject: [PATCH 20/70] Add ESMFold2 tests + sub-config model_types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESMFold2 is an all-atom structure predictor whose forward takes ~18 structural feature tensors and returns a plain dict (not a ModelOutput), so it doesn't fit ModelTesterMixin. Following the sanctioned pattern for such models, register the test file in check_repo's TEST_FILES_WITH_NO_COMMON_TESTS and provide focused coverage: - ESMFold2ConfigTest: ConfigTester common tests (incl. composite sub-config save/load), `type` validation, nested round-trip, attn-impl propagation. - ESMFold2ModelTest (CPU): full pure-PyTorch forward via infer_protein with no ESMC backbone (LM conditioning skipped) under both sdpa and eager; SWA config dispatch; weight-level save/load fidelity + a usable reloaded model. - ESMFold2IntegrationTest: @slow real-weight fold on biohub/ESMFold2 (GPU-gated). To make ConfigTester's composite test pass, each sub-config now declares a unique `model_type` (e.g. "esmfold2_inputs_embedder") — the CLIP pattern — so that `SubConfig.from_pretrained()` extracts the matching nested dict (configuration_utils keys this off model_type). ESM dodges this test because its sub-config is None by default; ESMFold2's are always present. check_repo: `modeling_esmfold2_common` (shared building blocks, no public model) is added to get_model_modules' _ignore_modules. The remaining check_repo item for esmfold2 is the model-doc page, which is a separate pending task. The tiny test config encodes two real sizing constraints discovered via the forward smoke: 3D RoPE needs 3*n_spatial + n_uid <= head_dim//2, and inputs.d_inputs == 67 + d_token//2 == diffusion_module.c_s_inputs. 7 non-slow tests pass; ruff + check_config_attributes + check_docstrings clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/configuration_esmfold2.py | 15 ++ tests/models/esmfold2/__init__.py | 0 .../models/esmfold2/test_modeling_esmfold2.py | 212 ++++++++++++++++++ utils/check_repo.py | 3 + 4 files changed, 230 insertions(+) create mode 100644 tests/models/esmfold2/__init__.py create mode 100644 tests/models/esmfold2/test_modeling_esmfold2.py diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 539a5b906b71..029530acf4fc 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -33,6 +33,8 @@ class MSAEncoderConfig(PreTrainedConfig): """Config for the optional MSA encoder module (Large MSA models only).""" + model_type = "esmfold2_msa_encoder" + enabled: bool | None = False d_msa: int | None = 128 d_hidden: int | None = 32 @@ -45,6 +47,8 @@ class MSAEncoderConfig(PreTrainedConfig): class ParcaeConfig(PreTrainedConfig): """Release-only config for the parcae diffusion-loop scheduler.""" + model_type = "esmfold2_parcae" + enabled: bool | None = True poisson_mean: float | None = 3.0 min_steps: int | None = 1 @@ -56,6 +60,8 @@ class ParcaeConfig(PreTrainedConfig): class LMEncoderConfig(PreTrainedConfig): """Release-only config for the LM-side pair encoder.""" + model_type = "esmfold2_lm_encoder" + enabled: bool | None = True n_layers: int | None = 4 lm_dropout: float | None = 0.25 @@ -66,6 +72,8 @@ class LMEncoderConfig(PreTrainedConfig): class AtomAttentionConfig(PreTrainedConfig): """Config for the SWA atom encoder/decoder with 3D RoPE.""" + model_type = "esmfold2_atom_attention" + d_atom: int | None = 128 d_token: int | None = 768 n_blocks: int | None = 3 @@ -82,6 +90,8 @@ class AtomAttentionConfig(PreTrainedConfig): class FoldingTrunkConfig(PreTrainedConfig): """Config for a pairwise folding trunk stack.""" + model_type = "esmfold2_folding_trunk" + n_layers: int | None = 24 n_heads: int | None = 8 dropout: float | None = 0.0 @@ -91,6 +101,7 @@ class FoldingTrunkConfig(PreTrainedConfig): class InputsEmbedderConfig(PreTrainedConfig): """Config for the inputs embedder (wraps the atom encoder).""" + model_type = "esmfold2_inputs_embedder" sub_configs = {"atom_encoder": AtomAttentionConfig} d_inputs: int | None = 451 @@ -108,6 +119,8 @@ def __post_init__(self, **kwargs): class DiffusionModuleConfig(PreTrainedConfig): """Config for the DiffusionModule.""" + model_type = "esmfold2_diffusion_module" + sigma_data: float | None = 16.0 c_atom: int | None = 128 c_token: int | None = 768 @@ -127,6 +140,7 @@ class DiffusionModuleConfig(PreTrainedConfig): class DiffusionStructureHeadConfig(PreTrainedConfig): """Config for the diffusion-based structure prediction head.""" + model_type = "esmfold2_structure_head" sub_configs = {"diffusion_module": DiffusionModuleConfig} diffusion_module: dict | DiffusionModuleConfig | None = None @@ -157,6 +171,7 @@ def __post_init__(self, **kwargs): class ConfidenceHeadConfig(PreTrainedConfig): """Config for the confidence prediction head.""" + model_type = "esmfold2_confidence_head" sub_configs = {"folding_trunk": FoldingTrunkConfig} enabled: bool | None = True diff --git a/tests/models/esmfold2/__init__.py b/tests/models/esmfold2/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py new file mode 100644 index 000000000000..106b48c0cc72 --- /dev/null +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -0,0 +1,212 @@ +# Copyright 2026 Biohub and The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Testing suite for the PyTorch ESMFold2 model. + +ESMFold2 is an all-atom structure predictor: its forward takes ~18 structural +feature tensors (built from a sequence by ``prepare_protein_features``) and +returns a plain ``dict`` rather than a ``ModelOutput``, so it does not plug into +``ModelTesterMixin`` (the file is registered in +``utils/check_repo.py::TEST_FILES_WITH_NO_COMMON_TESTS``). Coverage here is the +config (round-trip / nesting), a CPU forward smoke test across attention +backends, weight save/load, and a slow real-weight integration test. +""" + +import tempfile +import unittest + +from transformers import ESMFold2Config, is_torch_available +from transformers.testing_utils import ( + TestCasePlus, + require_torch, + require_torch_accelerator, + slow, + torch_device, +) + +from ...test_configuration_common import ConfigTester + + +if is_torch_available(): + import torch + + from transformers import ESMFold2Model + from transformers.models.esmfold2.modeling_esmfold2_common import SWA3DRoPEAttention + + +def get_tiny_config(**overrides) -> "ESMFold2Config": + """A minimal but internally consistent ESMFold2 config for CPU testing. + + Constraints (see modeling): 3D RoPE needs ``3*n_spatial + n_uid <= head_dim//2`` + (head_dim = d_atom/n_heads = c_atom/atom_num_heads = 8 here), and + ``inputs.d_inputs == 67 + d_token//2 == structure_head.diffusion_module.c_s_inputs`` + (the feature-concat width; 83 = 67 + 32//2). + """ + kwargs = { + "d_single": 32, + "d_pair": 16, + "num_loops": 1, + "num_diffusion_samples": 1, + "lm_d_model": 32, + "lm_num_layers": 1, + "inputs": { + "d_inputs": 83, + "atom_encoder": { + "d_atom": 16, + "d_token": 32, + "n_blocks": 1, + "n_heads": 2, + "swa_window_size": 8, + "n_spatial_rope_pairs_per_axis": 1, + "n_uid_rope_pairs": 1, + }, + }, + "folding_trunk": {"n_layers": 1, "n_heads": 2}, + "structure_head": { + "diffusion_module": { + "c_atom": 16, + "c_token": 32, + "c_z": 16, + "c_s_inputs": 83, + "atom_num_blocks": 1, + "atom_num_heads": 2, + "token_num_blocks": 1, + "token_num_heads": 2, + }, + "distogram_bins": 8, + }, + "confidence_head": { + "num_plddt_bins": 4, + "num_pde_bins": 4, + "num_pae_bins": 4, + "distogram_bins": 8, + "folding_trunk": {"n_layers": 1, "n_heads": 2}, + }, + "parcae": {"coda_n_layers": 1}, + "lm_encoder": {"n_layers": 1}, + } + kwargs.update(overrides) + return ESMFold2Config(**kwargs) + + +@require_torch +class ESMFold2ConfigTest(unittest.TestCase): + def setUp(self): + # ESMFold2Config is composite (sub_configs) with no vocab/hidden_size. + self.config_tester = ConfigTester(self, config_class=ESMFold2Config, has_text_modality=False, num_loops=5) + + def test_config(self): + self.config_tester.run_common_tests() + + def test_type_validation(self): + # Only the "release" variant is supported in this port. + ESMFold2Config(type="release") + with self.assertRaises(ValueError): + ESMFold2Config(type="experimental") + + def test_nested_config_round_trip(self): + config = ESMFold2Config(d_pair=72, inputs={"d_inputs": 99, "atom_encoder": {"d_atom": 64, "n_heads": 8}}) + with tempfile.TemporaryDirectory() as tmp: + config.save_pretrained(tmp) + reloaded = ESMFold2Config.from_pretrained(tmp) + + self.assertEqual(reloaded.to_dict(), config.to_dict()) + # Sub-configs round-trip as the right (PreTrainedConfig) types, not dicts. + self.assertEqual(type(reloaded.inputs).__name__, "InputsEmbedderConfig") + self.assertEqual(type(reloaded.inputs.atom_encoder).__name__, "AtomAttentionConfig") + self.assertEqual(reloaded.inputs.d_inputs, 99) + self.assertEqual(reloaded.inputs.atom_encoder.d_atom, 64) + + def test_attn_implementation_propagates_to_subconfigs(self): + config = ESMFold2Config(attn_implementation="sdpa") + self.assertEqual(config._attn_implementation, "sdpa") + self.assertEqual(config.inputs._attn_implementation, "sdpa") + + +@require_torch +class ESMFold2ModelTest(unittest.TestCase): + seq = "MKLVAAG" + + # These are pure-PyTorch correctness smoke tests, run on CPU for portability + # (the diffusion sampler is tiny here); GPU is covered by the slow integration + # test below. + def _build(self, attn_implementation="sdpa"): + torch.manual_seed(0) + config = get_tiny_config(attn_implementation=attn_implementation) + return ESMFold2Model(config).eval() + + def test_forward_runs_on_both_backends(self): + # No ESMC backbone is loaded -> LM conditioning is skipped (a valid path), + # so this exercises the full pure-PyTorch structural stack on CPU. + for impl in ("sdpa", "eager"): + with self.subTest(attn_implementation=impl): + model = self._build(impl) + self.assertIsNone(model._esmc) + with torch.no_grad(): + out = model.infer_protein(self.seq, num_loops=1, num_diffusion_samples=1, num_sampling_steps=2) + coords = out["sample_atom_coords"] + self.assertEqual(coords.shape[0], 1) # num_diffusion_samples + self.assertEqual(coords.shape[-1], 3) # xyz + self.assertTrue(torch.isfinite(coords).all()) + self.assertEqual(out["distogram_logits"].shape[-1], model.config.structure_head.distogram_bins) + + def test_attention_dispatch_attached(self): + model = self._build("eager") + swa_modules = [m for m in model.modules() if isinstance(m, SWA3DRoPEAttention)] + # Both atom sites (inputs embedder + diffusion decoder) contribute SWA modules. + self.assertGreaterEqual(len(swa_modules), 1) + self.assertTrue(all(m.config is model.config for m in swa_modules)) + self.assertTrue(all(m.config._attn_implementation == "eager" for m in swa_modules)) + + def test_save_load(self): + # The forward is intentionally stochastic (parcae diffusion-loop scheduler), + # so save/load fidelity is checked at the weight level, then the reloaded + # model is run to confirm it is usable. + model = self._build() + state_before = model.state_dict() + + with tempfile.TemporaryDirectory() as tmp: + model.save_pretrained(tmp) + # load_esmc=False: skip auto-loading the (real, multi-GB) ESMC backbone; + # the saved model has no backbone either, so both take the no-LM path. + reloaded = ESMFold2Model.from_pretrained(tmp, load_esmc=False).eval() + + state_after = reloaded.state_dict() + self.assertEqual(set(state_before), set(state_after)) + for key, tensor in state_before.items(): + torch.testing.assert_close(state_after[key], tensor, rtol=0, atol=0) + + with torch.no_grad(): + out = reloaded.infer_protein(self.seq, num_loops=1, num_diffusion_samples=1, num_sampling_steps=1) + self.assertTrue(torch.isfinite(out["sample_atom_coords"]).all()) + + +@require_torch +class ESMFold2IntegrationTest(TestCasePlus): + @slow + @require_torch_accelerator + def test_inference_protein_folding(self): + # from_pretrained auto-loads the ESMC backbone (load_esmc=True by default). + model = ESMFold2Model.from_pretrained("biohub/ESMFold2").to(torch_device).eval() + + seq = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKR" + with torch.no_grad(): + output = model.infer_protein(seq, num_diffusion_samples=1) + + coords = output["sample_atom_coords"] + plddt = output["plddt"] + self.assertEqual(coords.shape[-1], 3) + self.assertTrue(torch.isfinite(coords).all()) + # pLDDT is a 0-100 confidence; a real fold of this sequence should be confident. + self.assertTrue((plddt >= 0).all() and (plddt <= 100).all()) + self.assertGreater(plddt.mean().item(), 50.0) diff --git a/utils/check_repo.py b/utils/check_repo.py index 76e1025dd923..00afb1ea234b 100644 --- a/utils/check_repo.py +++ b/utils/check_repo.py @@ -306,6 +306,7 @@ # trigger the common tests. TEST_FILES_WITH_NO_COMMON_TESTS = [ "models/decision_transformer/test_modeling_decision_transformer.py", + "models/esmfold2/test_modeling_esmfold2.py", "models/camembert/test_modeling_camembert.py", "models/mbart/test_modeling_mbart.py", "models/mt5/test_modeling_mt5.py", @@ -653,6 +654,8 @@ def get_model_modules() -> list[str]: _ignore_modules = [ "modeling_auto", "modeling_encoder_decoder", + # Shared ESMFold2 building blocks; the public model is in modeling_esmfold2. + "modeling_esmfold2_common", "modeling_marian", "modeling_retribert", "modeling_speech_encoder_decoder", From 9bc2602232abd04bc2190be5d417a6aef81eff32 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 18:39:27 +0100 Subject: [PATCH 21/70] Add ESMFold2 model doc page Short overview page (mirrors esmc.md): describes the all-atom structure predictor and its separately-loaded ESMC backbone, a usage snippet (infer_protein_as_pdb), and autodoc for ESMFold2Config + ESMFold2Model. Registered in _toctree.yml after ESMC. Resolves the final check_repo "objects documented" item for esmfold2. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/source/en/_toctree.yml | 2 + docs/source/en/model_doc/esmfold2.md | 59 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 docs/source/en/model_doc/esmfold2.md diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index e52623830af2..278ef2b1be8e 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -603,6 +603,8 @@ title: ESM - local: model_doc/esmc title: ESMC + - local: model_doc/esmfold2 + title: ESMFold2 - local: model_doc/eurobert title: EuroBERT - local: model_doc/exaone4 diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md new file mode 100644 index 000000000000..9df1c85cf042 --- /dev/null +++ b/docs/source/en/model_doc/esmfold2.md @@ -0,0 +1,59 @@ + + +# ESMFold2 + +## Overview + +ESMFold2 is an all-atom protein structure prediction model. It predicts 3D coordinates and per-residue confidence +(pLDDT, PAE, PDE) directly from an amino-acid sequence, using the [ESMC](./esmc) protein language model as its +backbone. The architecture combines a sliding-window atom encoder with 3D rotary position embeddings, a pairwise +folding trunk applied iteratively (the "parcae" recurrence), a diffusion-based structure head, and a confidence head. + +The ESMC backbone is **not bundled** in the ESMFold2 checkpoint; it is loaded separately from the repository named by +`config.esmc_id` (default [`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B)). +[`ESMFold2Model.from_pretrained`](#transformers.ESMFold2Model) loads it automatically — pass `load_esmc=False` to skip +this (e.g. when supplying precomputed language-model hidden states). + +The model checkpoint is available on the Hugging Face Hub at +[`biohub/ESMFold2`](https://huggingface.co/biohub/ESMFold2). + +## Usage example + +```python +from transformers import ESMFold2Model + +# Loading the model also downloads and attaches the ESMC backbone (config.esmc_id). +model = ESMFold2Model.from_pretrained("biohub/ESMFold2").cuda().eval() + +pdb_string = model.infer_protein_as_pdb("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ") +with open("prediction.pdb", "w") as f: + f.write(pdb_string) +``` + +`infer_protein` returns the raw outputs (atom coordinates, distogram logits and confidence metrics) as a dictionary if +you need them instead of a PDB string. Generation is stochastic — set a manual seed for reproducible structures. + +## ESMFold2Config + +[[autodoc]] ESMFold2Config + +## ESMFold2Model + +[[autodoc]] ESMFold2Model + - forward + - infer_protein + - infer_protein_as_pdb From 47d0099b8a4e177ba10d946d829695e24e1c36e3 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 18:39:27 +0100 Subject: [PATCH 22/70] Ruff-format the ESMFold2 fork files (deferred style sweep) Runs the previously-deferred ruff sweep over the esmfold2 files now that the substantive work (TE removal, attention interface, config) is done: drops the `# coding=utf-8` lines (UP009), rewrites the `_msa_kwargs = dict(...)` as a literal (C408), sorts imports, and applies `ruff format`. No logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/transformers/models/esmfold2/__init__.py | 5 +- .../models/esmfold2/modeling_esmfold2.py | 197 ++++--------- .../esmfold2/modeling_esmfold2_common.py | 275 +++++------------- .../models/esmfold2/protein_utils.py | 18 +- 4 files changed, 123 insertions(+), 372 deletions(-) diff --git a/src/transformers/models/esmfold2/__init__.py b/src/transformers/models/esmfold2/__init__.py index 1b4fd78e45c6..cc3c471c1971 100644 --- a/src/transformers/models/esmfold2/__init__.py +++ b/src/transformers/models/esmfold2/__init__.py @@ -16,6 +16,7 @@ from ...utils import _LazyModule # type: ignore[import] from ...utils.import_utils import define_import_structure # type: ignore[import] + if TYPE_CHECKING: from .configuration_esmfold2 import * # noqa: F403 from .modeling_esmfold2 import * # noqa: F403 @@ -23,6 +24,4 @@ import sys _file = globals()["__file__"] - sys.modules[__name__] = _LazyModule( - __name__, _file, define_import_structure(_file), module_spec=__spec__ - ) + sys.modules[__name__] = _LazyModule(__name__, _file, define_import_structure(_file), module_spec=__spec__) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 56e365f3bfd5..d63bced83f15 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -106,21 +106,15 @@ def __init__(self, config: "ESMFold2Config") -> None: self.s_inputs_norm = nn.LayerNorm(d_inputs) self.z_norm = nn.LayerNorm(d_pair) - self.row_attention_pooling = RowAttentionPooling( - d_pair=d_pair, d_single=d_single - ) + self.row_attention_pooling = RowAttentionPooling(d_pair=d_pair, d_single=d_single) pf = ch.folding_trunk - self.folding_trunk = FoldingTrunk( - n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 - ) + self.folding_trunk = FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) # Heads. self.plddt_ln = nn.LayerNorm(d_single) max_atoms_per_token = 23 - self.plddt_weight = nn.Parameter( - torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins) - ) + self.plddt_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)) self.pae_ln = nn.LayerNorm(d_pair) self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) @@ -130,20 +124,14 @@ def __init__(self, config: "ESMFold2Config") -> None: self.resolved_ln = nn.LayerNorm(d_single) # 2 = resolved logits ([unresolved, resolved]). - self.resolved_weight = nn.Parameter( - torch.zeros(max_atoms_per_token, d_single, 2) - ) + self.resolved_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, 2)) def set_chunk_size(self, chunk_size: int | None) -> None: self.folding_trunk.set_chunk_size(chunk_size) @staticmethod def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor: - return ( - x - if num_diffusion_samples == 1 - else x.repeat_interleave(num_diffusion_samples, 0) - ) + return x if num_diffusion_samples == 1 else x.repeat_interleave(num_diffusion_samples, 0) @staticmethod def _flatten_sample_axis(x: Tensor) -> Tensor: @@ -177,8 +165,7 @@ def forward( z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2) z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1) z_base = z_base + self.s_to_z_prod_out( - self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] - * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] + self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] ) pair = self._repeat_batch(z_base, num_diffusion_samples) @@ -190,12 +177,8 @@ def forward( Bm = pair.shape[0] rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) - rep_distances = torch.cdist( - rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist" - ) - distogram_bins = ( - (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() - ) + rep_distances = torch.cdist(rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist") + distogram_bins = (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() pair = pair + self.dist_bin_pairwise_embed(distogram_bins) pair_mask = mask[:, :, None].float() * mask[:, None, :].float() @@ -221,40 +204,28 @@ def forward( L = single.shape[1] plddt_sum = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) - atom_count = torch.zeros( - Bm, L, device=single.device, dtype=plddt_per_atom.dtype - ) + atom_count = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype) plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t) atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) plddt = plddt_sum / atom_count.clamp(min=1e-6) - complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / ( - atom_mask_f.sum(dim=-1) + _EPS - ) + complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (atom_mask_f.sum(dim=-1) + _EPS) expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) is_ligand = (expanded_type == _NONPOLYMER_ID).float() - inter_chain = ( - expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) - ).float() + inter_chain = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() near_contact = (rep_distances < 8).float() - interface_per_token = ( - near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1) - ).amax(dim=-1) + interface_per_token = (near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1)).amax(dim=-1) iplddt_weight = torch.where( is_ligand.bool(), torch.full_like(interface_per_token, 2.0), interface_per_token, ) - iplddt_weight_atoms = gather_token_to_atom( - iplddt_weight.unsqueeze(-1), atom_to_token_m - ).squeeze(-1) + iplddt_weight_atoms = gather_token_to_atom(iplddt_weight.unsqueeze(-1), atom_to_token_m).squeeze(-1) atom_iplddt_w = atom_mask_f * iplddt_weight_atoms - complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / ( - atom_iplddt_w.sum(dim=-1) + _EPS - ) + complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (atom_iplddt_w.sum(dim=-1) + _EPS) plddt_ca = plddt_per_atom.gather(1, rep_idx_m) @@ -274,9 +245,7 @@ def forward( # pTM / ipTM from pae_logits. n_bins = pae_logits.shape[-1] bin_width = 32.0 / n_bins - bin_centers = torch.arange( - 0.5 * bin_width, 32.0, bin_width, device=pae_logits.device - ) + bin_centers = torch.arange(0.5 * bin_width, 32.0, bin_width, device=pae_logits.device) mask_f = mask.float() N_res = mask_f.sum(dim=-1, keepdim=True) d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 @@ -285,24 +254,16 @@ def forward( tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1) pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) - ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / ( - pair_mask_2d.sum(dim=-1) + _EPS - ) + ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (pair_mask_2d.sum(dim=-1) + _EPS) ptm = ptm_per_row.max(dim=-1).values - inter_chain_mask = ( - expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2) - ).float() * pair_mask_2d - iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / ( - inter_chain_mask.sum(dim=-1) + _EPS - ) + inter_chain_mask = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() * pair_mask_2d + iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (inter_chain_mask.sum(dim=-1) + _EPS) iptm = iptm_per_row.max(dim=-1).values max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 n_chains = max_chain_id + 1 - pair_chains_iptm = torch.zeros( - Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype - ) + pair_chains_iptm = torch.zeros(Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype) for c1 in range(n_chains): chain_c1 = (expanded_asym == c1).float() * mask_f if chain_c1.sum() == 0: @@ -311,9 +272,7 @@ def forward( chain_c2 = (expanded_asym == c2).float() * mask_f pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) denom = pair_m.sum(dim=(-1, -2)) + _EPS - pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum( - dim=(-1, -2) - ) / denom + pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(dim=(-1, -2)) / denom return { "plddt_logits": plddt_logits, @@ -387,15 +346,11 @@ def __init__(self, config: ESMFold2Config) -> None: d_pair=d_pair, ) self.token_bonds = nn.Linear(1, d_pair, bias=False) - self.language_model = LanguageModelShim( - d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers - ) + self.language_model = LanguageModelShim(d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers) self._esmc: nn.Module | None = None pf = config.folding_trunk - self.folding_trunk = FoldingTrunk( - n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4 - ) + self.folding_trunk = FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) if config.lm_encoder.enabled: self.lm_encoder: FoldingTrunk | None = FoldingTrunk( n_layers=config.lm_encoder.n_layers, d_pair=d_pair, expansion_ratio=4 @@ -408,22 +363,16 @@ def __init__(self, config: ESMFold2Config) -> None: parcae_decay_init = math.sqrt(1.0 / 5.0) parcae_delta_init = -math.log(parcae_decay_init) self.parcae_log_delta = nn.Parameter( - torch.full( - (d_pair,), _inverse_softplus(parcae_delta_init), dtype=torch.float32 - ) + torch.full((d_pair,), _inverse_softplus(parcae_delta_init), dtype=torch.float32) ) self.parcae_b_cont = nn.Parameter(torch.eye(d_pair)) self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) nn.init.eye_(self.parcae_readout.weight) - self.parcae_coda = FoldingTrunk( - n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4 - ) + self.parcae_coda = FoldingTrunk(n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4) # Heads -------------------------------------------------------------- self.structure_head = DiffusionStructureHead(config) - self.distogram_head = nn.Linear( - d_pair, config.structure_head.distogram_bins, bias=True - ) + self.distogram_head = nn.Linear(d_pair, config.structure_head.distogram_bins, bias=True) self.confidence_head = ConfidenceHead(config) msa_cfg = config.msa_encoder @@ -462,29 +411,19 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: "fp32": torch.float32, } if precision not in dtype_map: - raise ValueError( - f"precision must be one of {list(dtype_map)}, got {precision!r}" - ) + raise ValueError(f"precision must be one of {list(dtype_map)}, got {precision!r}") dtype = dtype_map[precision] - esmc = ( - AutoModel.from_pretrained(esmc_model_path) - .to(device=self.device, dtype=dtype) - .eval() - ) + esmc = AutoModel.from_pretrained(esmc_model_path).to(device=self.device, dtype=dtype).eval() for p in esmc.parameters(): p.requires_grad_(False) self._esmc = esmc @classmethod - def from_pretrained( - cls, pretrained_model_name_or_path, *args, load_esmc: bool = True, **kwargs - ): + def from_pretrained(cls, pretrained_model_name_or_path, *args, load_esmc: bool = True, **kwargs): if cls is ESMFold2Model and "config" not in kwargs: - config = ESMFold2Config.from_pretrained( - pretrained_model_name_or_path, **kwargs - ) + config = ESMFold2Config.from_pretrained(pretrained_model_name_or_path, **kwargs) kwargs["config"] = config # Pop the precision knob before forwarding to the HF loader. esmc_precision = kwargs.pop("esmc_precision", "bf16") @@ -493,9 +432,7 @@ def from_pretrained( model.load_esmc(model.config.esmc_id, precision=esmc_precision) return model - def apply_torch_compile( - self, mode: str = "fixed_seqlen", dynamic: bool | None = None - ) -> None: + def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = None) -> None: """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" import torch._dynamo @@ -600,23 +537,15 @@ def _run_one_loop( refined_lm_z: Tensor | None = None if lm_z_i is not None and self.lm_encoder is not None: - refined_lm_z = self.lm_encoder( - lm_z_i.to(z_init.dtype), pair_attention_mask=pair_mask - ) + refined_lm_z = self.lm_encoder(lm_z_i.to(z_init.dtype), pair_attention_mask=pair_mask) z_inject_pair = z_init if lm_z_i is not None and self.lm_encoder is None: z_inject_pair = z_inject_pair + lm_z_i.to(z_inject_pair.dtype) if self.msa_encoder is not None and _msa_kwargs is not None: - msa_pair = self.msa_encoder(x_pair=z_inject_pair, **_msa_kwargs).to( - z_inject_pair.dtype - ) - z_inject_pair = ( - msa_pair - if self.config.msa_encoder_overwrite - else (z_inject_pair + msa_pair) - ) + msa_pair = self.msa_encoder(x_pair=z_inject_pair, **_msa_kwargs).to(z_inject_pair.dtype) + z_inject_pair = msa_pair if self.config.msa_encoder_overwrite else (z_inject_pair + msa_pair) if refined_lm_z is not None: z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) @@ -665,9 +594,7 @@ def forward( n_loops: int = num_loops if num_loops is not None else self.config.num_loops n_samples: int = ( - num_diffusion_samples - if num_diffusion_samples is not None - else self.config.num_diffusion_samples + num_diffusion_samples if num_diffusion_samples is not None else self.config.num_diffusion_samples ) total_steps = max(1, n_loops + 1) @@ -690,22 +617,14 @@ def forward( profile = res_type_oh if deletion_mean is None: - deletion_mean = torch.zeros( - res_type.shape[0], res_type.shape[1], device=res_type.device - ) + deletion_mean = torch.zeros(res_type.shape[0], res_type.shape[1], device=res_type.device) - ref_element_oh = F.one_hot( - ref_element.long(), num_classes=MAX_ATOMIC_NUMBER - ).float() - ref_atom_name_chars_oh = F.one_hot( - ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE - ).float() + ref_element_oh = F.one_hot(ref_element.long(), num_classes=MAX_ATOMIC_NUMBER).float() + ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE).float() # Bias-free downstream Linears require zeroed padding. atm_mask_f = atm_mask.float() ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) - ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze( - -1 - ).unsqueeze(-1) + ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(-1).unsqueeze(-1) atom_to_token = atom_to_token * atm_mask.long() use_amp = ref_pos.device.type == "cuda" @@ -723,9 +642,7 @@ def forward( atom_to_token=atom_to_token, ) - z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2( - x_inputs - ).unsqueeze(1) + z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(x_inputs).unsqueeze(1) relative_position_encoding = self.rel_pos( residue_index=residue_index, @@ -737,11 +654,7 @@ def forward( token_bonds_encoding = self.token_bonds(token_bonds.float()) z_init = z_init + relative_position_encoding + token_bonds_encoding - if ( - lm_hidden_states is None - and input_ids is not None - and self._esmc is not None - ): + if lm_hidden_states is None and input_ids is not None and self._esmc is not None: lm_hidden_states = self._compute_lm_hidden_states( input_ids, asym_id, residue_index, mol_type, tok_mask ) @@ -761,9 +674,7 @@ def forward( _msa_kwargs: dict | None = None if self.msa_encoder is not None and msa is not None: B_msa, M, L_msa = msa.shape - msa_oh = F.one_hot( - msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES - ).float() + msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES).float() msa_attn = ( msa_attention_mask.permute(0, 2, 1).float() if msa_attention_mask is not None @@ -781,13 +692,13 @@ def forward( if deletion_value is not None else torch.zeros(B_msa, L_msa, M, device=msa.device) ) - _msa_kwargs = dict( - x_inputs=x_inputs, - msa_oh=msa_oh, - has_deletion=hd, - deletion_value=dv, - msa_attention_mask=msa_attn, - ) + _msa_kwargs = { + "x_inputs": x_inputs, + "msa_oh": msa_oh, + "has_deletion": hd, + "deletion_value": dv, + "msa_attention_mask": msa_attn, + } # Method call (not inline loop) frees per-iter L²×c_z locals. z = self._run_one_loop( @@ -852,9 +763,7 @@ def forward( token_bonds_encoding=token_bonds_encoding.detach(), ) output.update(confidence_output) - output["atom_pad_mask"] = ( - atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask - ) + output["atom_pad_mask"] = atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask output["residue_index"] = residue_index output["entity_id"] = entity_id return output @@ -896,9 +805,7 @@ def __init__( self.is_final_block = is_final_block self.outer_product_mean = OuterProductMean(d_msa, d_hidden, d_pair) if not is_final_block: - self.msa_pair_weighted_averaging = MSAPairWeightedAveraging( - d_msa, d_pair, n_heads_msa, msa_head_width - ) + self.msa_pair_weighted_averaging = MSAPairWeightedAveraging(d_msa, d_pair, n_heads_msa, msa_head_width) self.msa_transition = PairTransition(d_msa, expansion_ratio=4) self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) @@ -973,9 +880,7 @@ def forward( msa_attention_mask: Tensor, ) -> Tensor: # All inputs are pre-transposed to [B, L, M, ...] before calling. - m_feat = torch.cat( - [msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1 - ) + m_feat = torch.cat([msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1) m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2) tok_mask = msa_attention_mask[:, :, 0].bool() pair_attention_mask = tok_mask.unsqueeze(2) & tok_mask.unsqueeze(1) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 28a1ceeb8c8f..f9c46add76e5 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -1,4 +1,3 @@ -# coding=utf-8 # Copyright 2026 Biohub. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -75,9 +74,7 @@ def forward(self, residual: Tensor, delta: Tensor) -> Tensor: MAX_ATOMIC_NUMBER: int = 128 # Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 -ATOM_FEATURE_DIM: int = ( - XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS -) +ATOM_FEATURE_DIM: int = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS NUM_RES_TYPES: int = 33 @@ -135,12 +132,8 @@ def scatter_atom_to_token( idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) n_out = n_tokens + 1 idx_expanded = idx.unsqueeze(-1).expand(B, A, d) - out = torch.zeros( - B, n_out, d, device=atom_features.device, dtype=atom_features.dtype - ) - out.scatter_reduce_( - 1, idx_expanded, atom_features, reduce="mean", include_self=False - ) + out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) + out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) return out[:, :n_tokens, :] @@ -170,9 +163,7 @@ def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: Returns: [B, A] tensor with values in [0, max_atoms_per_token - 1]. """ - same_as_prev = F.pad( - atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False - ) + same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) ones = torch.ones_like(atom_to_token) cumsum = torch.cumsum(ones, dim=-1) group_start = cumsum.masked_fill(same_as_prev, 0) @@ -194,9 +185,7 @@ def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: [...] expected value """ n_bins = logits.shape[-1] - edges = torch.linspace( - start, end, n_bins + 1, device=logits.device, dtype=torch.float32 - ) + edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) @@ -266,9 +255,7 @@ def __init__(self, c: int) -> None: def forward(self, t_hat: Tensor) -> Tensor: t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) - return torch.cos( - 2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :]) - ) + return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) # =========================================================================== @@ -306,13 +293,9 @@ def forward(self, x: Tensor) -> Tensor: class SwiGLUMLP(SwiGLU): """SwiGLU MLP with packed weights, no bias.""" - def __init__( - self, d_model: int, expansion_ratio: int = 4, bias: bool = False - ) -> None: + def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: hidden = _compute_swiglu_hidden_size(d_model, expansion_ratio) - super().__init__( - in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias - ) + super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) # =========================================================================== @@ -360,17 +343,10 @@ def build_3d_rope( spatial_inv_freq = 1.0 / ( spatial_base_freq - ** ( - torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) - / n_spatial_per_axis - ) + ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) ) uid_inv_freq = 1.0 / ( - uid_base_freq - ** ( - torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) - / n_uid_pairs - ) + uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) ) pos_f32 = ref_pos.float() @@ -384,9 +360,7 @@ def build_3d_rope( freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) if n_active < half_dim: - padding = torch.zeros( - B, N, half_dim - n_active, device=device, dtype=torch.float32 - ) + padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) freqs = torch.cat([freqs, padding], dim=-1) cos = freqs.cos().to(torch.bfloat16) @@ -441,12 +415,8 @@ def eager_attention_forward( attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling if attention_mask is not None: attn_weights = attn_weights + attention_mask - attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( - query.dtype - ) - attn_weights = nn.functional.dropout( - attn_weights, p=dropout, training=module.training - ) + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) attn_output = torch.matmul(attn_weights, value) attn_output = attn_output.transpose(1, 2).contiguous() return attn_output, attn_weights @@ -499,9 +469,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: if q.dtype not in (torch.float16, torch.bfloat16): q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() - attn_impl = ( - self.config._attn_implementation if self.config is not None else "sdpa" - ) + attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" use_flash = attn_impl == "flash_attention_2" and FLASH_ATTN_AVAILABLE if use_flash and len(attention_params) > 2: @@ -552,15 +520,11 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: allowed |= torch.eye(N, dtype=torch.bool, device=q.device) # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) - attn_mask = attn_mask.masked_fill( - ~allowed.unsqueeze(1), torch.finfo(q.dtype).min - ) + attn_mask = attn_mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(q.dtype).min) attention_interface: Callable = eager_attention_forward if attn_impl != "eager": - attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( - attn_impl, eager_attention_forward - ) + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) out, _ = attention_interface( self, q.transpose(1, 2), @@ -615,14 +579,8 @@ def __init__( self.attn = SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) self.ffn = SwiGLUFFN(d_atom, expansion_ratio) - self._rms_adaln = ( - torch.compile(_rms_adaln_raw) if use_compile_fusions else _rms_adaln_raw - ) - self._gated_residual = ( - torch.compile(_gated_residual_raw) - if use_compile_fusions - else _gated_residual_raw - ) + self._rms_adaln = torch.compile(_rms_adaln_raw) if use_compile_fusions else _rms_adaln_raw + self._gated_residual = torch.compile(_gated_residual_raw) if use_compile_fusions else _gated_residual_raw def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: mod = self.adaln_modulation(c_l) @@ -675,9 +633,7 @@ def __init__( ] ) - def _build_3d_rope( - self, ref_pos: Tensor, ref_space_uid: Tensor - ) -> tuple[Tensor, Tensor]: + def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: return build_3d_rope( ref_pos=ref_pos, ref_space_uid=ref_space_uid, @@ -818,9 +774,7 @@ def forward( layer_cache["attention_params"] = attention_params layer_cache["mask_exp"] = mask_exp layer_cache["n_tokens"] = n_tokens - layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave( - num_diffusion_samples, 0 - ) + layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave(num_diffusion_samples, 0) else: c_base = layer_cache["c_base"] attention_params = layer_cache["attention_params"] @@ -857,12 +811,8 @@ def forward( if layer_cache is not None and "atom_to_token_exp" in layer_cache: atom_to_token_exp = layer_cache["atom_to_token_exp"] else: - atom_to_token_exp = atom_to_token.repeat_interleave( - num_diffusion_samples, 0 - ) - a = scatter_atom_to_token( - q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool() - ) + atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) return a, q, c, attention_params, intermediates @@ -1006,19 +956,11 @@ def forward( # Expand z for num_diffusion_samples if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: z = z.repeat_interleave(num_diffusion_samples, dim=0) - if ( - attention_mask is not None - and attention_mask.shape[0] != bsz - and num_diffusion_samples > 1 - ): - attention_mask = attention_mask.repeat_interleave( - num_diffusion_samples, dim=0 - ) + if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: + attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) # Standard attention with pair bias - g = torch.sigmoid(self.g_proj(x)).view( - bsz, n_queries, self.num_heads, self.head_dim - ) + g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale @@ -1030,9 +972,7 @@ def forward( if attention_mask is not None: min_val = torch.finfo(logits.dtype).min - mask_bias = torch.where( - attention_mask.bool()[:, None, :, None], 0.0, min_val - ) + mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) logits = logits + mask_bias.to(dtype=logits.dtype) attn = torch.softmax(logits, dim=-2).to(dtype=v.dtype) @@ -1189,10 +1129,7 @@ def __init__( self.z_input_norm = nn.LayerNorm(2 * c_z, eps=layer_norm_eps) self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) self.z_transitions = nn.ModuleList( - [ - TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) - for _ in range(2) - ] + [TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] ) self.s_input_norm = nn.LayerNorm(c_s_inputs, eps=layer_norm_eps) @@ -1201,10 +1138,7 @@ def __init__( self.noise_norm = nn.LayerNorm(fourier_dim, eps=layer_norm_eps) self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) self.s_transitions = nn.ModuleList( - [ - TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) - for _ in range(2) - ] + [TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] ) def forward( @@ -1373,9 +1307,7 @@ def forward( ) -> dict[str, Tensor | None]: bsz = x_noisy.shape[0] sigma = self.sigma_data if sigma_data is None else float(sigma_data) - t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape( - -1 - ) + t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape(-1) if t.numel() == 1: t = t.expand(bsz) @@ -1507,9 +1439,7 @@ def __init__(self, config: ESMFold2Config) -> None: # Helpers # ------------------------------------------------------------------ - def inference_noise_schedule( - self, num_steps: int | None = None, device: torch.device | None = None - ) -> Tensor: + def inference_noise_schedule(self, num_steps: int | None = None, device: torch.device | None = None) -> Tensor: """Karras power-law noise schedule.""" steps = self.inference_num_steps if num_steps is None else int(num_steps) if steps == 1: @@ -1574,9 +1504,7 @@ def _center_random_augmentation( return x, second_coords @staticmethod - def _weighted_rigid_align( - x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor - ) -> Tensor: + def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> Tensor: """Kabsch alignment: align x to x_gt with weights w.""" w = (mask * w).unsqueeze(-1) # [B, N, 1] denom = w.sum(dim=-2, keepdim=True).clamp(min=1e-8) @@ -1589,9 +1517,7 @@ def _weighted_rigid_align( U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) det = torch.linalg.det(U @ Vh) ones = torch.ones_like(det) - R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to( - H.dtype - ) + R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to(H.dtype) return x_c @ R.transpose(-1, -2) + mu_gt # ------------------------------------------------------------------ @@ -1641,11 +1567,7 @@ def sample( inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None - steps = ( - self.inference_num_steps - if num_sampling_steps is None - else int(num_sampling_steps) - ) + steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) schedule = self.inference_noise_schedule(steps, device) if max_inference_sigma is not None: @@ -1655,9 +1577,7 @@ def sample( lam = self.noise_scale if noise_scale is None else float(noise_scale) eta = self.step_scale if step_scale is None else float(step_scale) - x = schedule[0] * torch.randn( - target_batch, n_atoms, 3, device=device, dtype=torch.float32 - ) + x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() gammas = torch.where( @@ -1674,9 +1594,7 @@ def sample( num_steps = len(step_pairs) for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): - x, x_denoised_prev = self._center_random_augmentation( - x, atom_mask, second_coords=x_denoised_prev - ) + x, x_denoised_prev = self._center_random_augmentation(x, atom_mask, second_coords=x_denoised_prev) sigma_tm_val = float(sigma_tm.item()) t_hat_val = sigma_tm_val * (1.0 + float(gamma.item())) @@ -1684,15 +1602,11 @@ def sample( x_noisy = x + eps_std * torch.randn_like(x) is_last_step = step_idx == num_steps - 1 - request_atom_repr = return_atom_repr and ( - is_last_step or denoising_early_exit_rmsd is not None - ) + request_atom_repr = return_atom_repr and (is_last_step or denoising_early_exit_rmsd is not None) dm_out = self.diffusion_module( x_noisy=x_noisy, - t_hat=torch.full( - (target_batch,), t_hat_val, device=device, dtype=torch.float32 - ), + t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), ref_pos=ref_pos, ref_charge=ref_charge, ref_mask=ref_mask, @@ -1723,9 +1637,7 @@ def sample( # Reverse diffusion alignment (Kabsch) with torch.autocast(device_type="cuda", enabled=False): - x_noisy = self._weighted_rigid_align( - x_noisy.float(), x_denoised.float(), atom_mask, atom_mask - ) + x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) x_noisy = x_noisy.to(dtype=x_denoised.dtype) # ODE/SDE step @@ -1734,11 +1646,7 @@ def sample( x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma # Denoising early-exit: stop when consecutive predictions converge - if ( - denoising_early_exit_rmsd is not None - and x_denoised_prev is not None - and step_idx >= 1 - ): + if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: with torch.autocast(device_type="cuda", enabled=False): aligned = self._weighted_rigid_align( x_denoised_prev.float(), @@ -1747,9 +1655,7 @@ def sample( atom_mask, ) diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) - per_sample_rmsd = ( - diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1) - ).sqrt() + per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: x = x_denoised x_denoised_prev = x_denoised @@ -1875,9 +1781,7 @@ def __init__( n_feats_token = 2 * n_relative_residx_bins + 2 n_feats_chain = 2 * n_relative_chain_bins + 2 n_feats_same_entity = 1 - total_feats = ( - n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity - ) + total_feats = n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity self.embed = nn.Linear(total_feats, d_pair, bias=False) def forward( @@ -1898,15 +1802,11 @@ def forward( 0, 2 * self.n_relative_residx_bins, ) - dij_residue = torch.where( - bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1 - ) + dij_residue = torch.where(bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1) aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) dij_token = torch.clip( - token_index.unsqueeze(2) - - token_index.unsqueeze(1) - + self.n_relative_residx_bins, + token_index.unsqueeze(2) - token_index.unsqueeze(1) + self.n_relative_residx_bins, 0, 2 * self.n_relative_residx_bins, ) @@ -1922,9 +1822,7 @@ def forward( 0, 2 * self.n_relative_chain_bins, ) - dij_chain = torch.where( - bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain - ) + dij_chain = torch.where(bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain) aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) feats = torch.cat( @@ -1980,15 +1878,11 @@ class LanguageModelShim(nn.Module): - base_z_mlp: Sequential(SingleToPair(d_z, d_z, d_z), LayerNorm(d_z)) """ - def __init__( - self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80 - ) -> None: + def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: super().__init__() self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z)) - self.base_z_linear = nn.Sequential( - nn.LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False) - ) + self.base_z_linear = nn.Sequential(nn.LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: @@ -2025,9 +1919,7 @@ def _seed_context(seed: int | None, *, cuda: bool = True): py_state = random.getstate() np_state = np.random.get_state() torch_state = torch.get_rng_state() - cuda_states = ( - torch.cuda.get_rng_state_all() if cuda and torch.cuda.is_available() else None - ) + cuda_states = torch.cuda.get_rng_state_all() if cuda and torch.cuda.is_available() else None seed = int(seed) % (2**32) random.seed(seed) np.random.seed(seed) @@ -2087,12 +1979,8 @@ def compute_lm_hidden_states( unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) n_unique = unique_keys.size(0) token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) - first_pos = torch.full( - (n_unique,), keys.size(0), device=device, dtype=torch.long - ) - first_pos.scatter_reduce_( - 0, inverse, token_positions, reduce="amin", include_self=True - ) + first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) + first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) ordered = torch.argsort(first_pos) first_pos_ordered = first_pos[ordered] ids_collapsed = ids_b[first_pos_ordered] @@ -2146,9 +2034,7 @@ def compute_lm_hidden_states( sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 with torch.inference_mode(): - esmc_out = esmc( - input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True - ) + esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) hs = esmc_out.hidden_states # [n_layers+1, B, max_len, D] n_layers_plus_1, _, _, D = hs.shape @@ -2175,9 +2061,7 @@ class TriangleMultiplicativeBlock(nn.Module): def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None: super().__init__() if flow not in self._FLOW_TO_EINSUM: - raise ValueError( - f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}." - ) + raise ValueError(f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}.") self.input_channels = input_channels self.latent_channels = latent_channels @@ -2185,12 +2069,8 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None self._einsum_equation = self._FLOW_TO_EINSUM[flow] self.norm_start = nn.LayerNorm(self.input_channels, eps=_EPS) self.norm_mix = nn.LayerNorm(self.latent_channels, eps=_EPS) - self.proj_bundle = nn.Linear( - self.input_channels, 4 * self.latent_channels, bias=False - ) - self.proj_emit = nn.Linear( - self.latent_channels, self.input_channels, bias=False - ) + self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) + self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) # Default chunked for memory on long sequences; tests override with @@ -2204,22 +2084,16 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: return torch.einsum(self._einsum_equation, left_stream, right_stream) - def _triangular_contract_chunked( - self, left_stream: Tensor, right_stream: Tensor, chunk_size: int - ) -> Tensor: + def _triangular_contract_chunked(self, left_stream: Tensor, right_stream: Tensor, chunk_size: int) -> Tensor: """Compute the triangular einsum in chunks along the output i-dimension.""" L = left_stream.shape[1] if self.flow == "outgoing" else left_stream.shape[2] chunks = [] for start in range(0, L, chunk_size): end = min(start + chunk_size, L) if self.flow == "outgoing": - chunk = torch.einsum( - self._einsum_equation, left_stream[:, start:end], right_stream - ) + chunk = torch.einsum(self._einsum_equation, left_stream[:, start:end], right_stream) else: - chunk = torch.einsum( - self._einsum_equation, left_stream[:, :, start:end], right_stream - ) + chunk = torch.einsum(self._einsum_equation, left_stream[:, :, start:end], right_stream) chunks.append(chunk) return torch.cat(chunks, dim=1) @@ -2235,9 +2109,7 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor left_stream, right_stream = routed.float().chunk(2, dim=-1) if self._chunk_size is not None: - contracted = self._triangular_contract_chunked( - left_stream, right_stream, self._chunk_size - ) + contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) else: contracted = self._triangular_contract(left_stream, right_stream) mixed = self.proj_emit(self.norm_mix(contracted)) @@ -2251,9 +2123,7 @@ class TriangleMultiplicativeUpdate(nn.Module): def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: super().__init__() flow = "outgoing" if _outgoing else "incoming" - self._engine = TriangleMultiplicativeBlock( - input_channels=dim, latent_channels=dim, flow=flow - ) + self._engine = TriangleMultiplicativeBlock(input_channels=dim, latent_channels=dim, flow=flow) def set_chunk_size(self, chunk_size: int | None) -> None: self._engine.set_chunk_size(chunk_size) @@ -2307,9 +2177,7 @@ def set_chunk_size(self, chunk_size: int | None) -> None: self.tri_mul_in.set_chunk_size(chunk_size) self.pair_transition.set_chunk_size(chunk_size) - def forward( - self, pair: Tensor, pair_attention_mask: Tensor | None = None - ) -> Tensor: + def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: pair = self.row_drop(pair, self.tri_mul_out(pair, mask=pair_attention_mask)) pair = self.row_drop(pair, self.tri_mul_in(pair, mask=pair_attention_mask)) pair = self.pair_transition(pair) @@ -2319,24 +2187,17 @@ def forward( class FoldingTrunk(nn.Module): """ModuleList of PairUpdateBlocks.""" - def __init__( - self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4 - ) -> None: + def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4) -> None: super().__init__() self.blocks = nn.ModuleList( - [ - PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) - for _ in range(n_layers) - ] + [PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) for _ in range(n_layers)] ) def set_chunk_size(self, chunk_size: int | None) -> None: for block in self.blocks: cast(PairUpdateBlock, block).set_chunk_size(chunk_size) - def forward( - self, pair: Tensor, pair_attention_mask: Tensor | None = None - ) -> Tensor: + def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: for block in self.blocks: fn = partial(block, pair_attention_mask=pair_attention_mask) if torch.is_grad_enabled(): @@ -2411,23 +2272,17 @@ def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: class MSAPairWeightedAveraging(nn.Module): """Pair-biased MSA row update (AF3 Supplement Algorithm 10).""" - def __init__( - self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32 - ) -> None: + def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32) -> None: super().__init__() self.n_heads = n_heads self.head_width = head_width self.norm_single = nn.LayerNorm(d_msa) - self.compute_bias = nn.Sequential( - nn.LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False) - ) + self.compute_bias = nn.Sequential(nn.LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) - def forward( - self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor - ) -> Tensor: + def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor) -> Tensor: """ Args: msa_repr: [B, L, M, d_msa] diff --git a/src/transformers/models/esmfold2/protein_utils.py b/src/transformers/models/esmfold2/protein_utils.py index 28aadf01e5c3..5fb332907fad 100644 --- a/src/transformers/models/esmfold2/protein_utils.py +++ b/src/transformers/models/esmfold2/protein_utils.py @@ -1,4 +1,3 @@ -# coding=utf-8 # Copyright 2026 Biohub. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +21,7 @@ import torch from torch import Tensor + MOL_TYPE_PROTEIN = 0 PROTEIN_UNK_RES_TYPE = 22 MSA_GAP_TOKEN_ID = 1 @@ -407,9 +407,7 @@ def prepare_protein_features(sequence: str) -> dict[str, Tensor]: atom_cursor += 1 rep_name = "CB" if "CB" in atom_names else "CA" - distogram_rep_atom_idx.append( - token_atom_starts[t_idx] + atom_names.index(rep_name) - ) + distogram_rep_atom_idx.append(token_atom_starts[t_idx] + atom_names.index(rep_name)) res_type_vals.append(res_type) input_id_vals.append(input_id) @@ -429,9 +427,7 @@ def prepare_protein_features(sequence: str) -> dict[str, Tensor]: ref_pos[i] = torch.tensor(pos, dtype=torch.float32) ref_element[i] = _PROTEIN_ELEMENT_TO_ATOMIC_NUM[element] ref_charge[i] = charge - ref_atom_name_chars[i] = torch.tensor( - _encode_atom_name(name), dtype=torch.int64 - ) + ref_atom_name_chars[i] = torch.tensor(_encode_atom_name(name), dtype=torch.int64) ref_space_uid[i] = t_idx atom_attention_mask[i] = True atom_to_token[i] = t_idx @@ -484,9 +480,7 @@ def prepare_protein_features(sequence: str) -> dict[str, Tensor]: # 0-32 res_type → 3-letter name (only protein indices 2-22 are populated). -_RES_TYPE_TO_3LETTER: dict[int, str] = { - rt: three for three, rt in PROTEIN_RESIDUE_TO_RES_TYPE.items() -} +_RES_TYPE_TO_3LETTER: dict[int, str] = {rt: three for three, rt in PROTEIN_RESIDUE_TO_RES_TYPE.items()} _RES_TYPE_TO_3LETTER[PROTEIN_UNK_RES_TYPE] = "UNK" # Featurization keys that ``output_to_pdb`` reads off the forward output. @@ -562,9 +556,7 @@ def output_to_pdb(output: dict) -> str: if tok not in tok_to_new: continue new_i = tok_to_new[tok] - name = "".join( - chr(int(c) + 32) if int(c) != 0 else " " for c in ref_chars[a] - ).strip() + name = "".join(chr(int(c) + 32) if int(c) != 0 else " " for c in ref_chars[a]).strip() idx37 = rc.atom_order.get(name) if idx37 is None: continue From d442c1eea809f3bf120b571005c8939e10fd3bcc Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 18:39:27 +0100 Subject: [PATCH 23/70] Convert ESMCConfig to the v5 @strict / @auto_docstring style ESMCConfig was the last non-@strict direct config (old def __init__ style), flagged by the TRF010 model-structure rule (make typing). Convert it to the dataclass `@strict` form with class-level typed fields, mirroring ESM, and switch the docstring to `@auto_docstring(checkpoint="biohub/ESMC-600M")` so the standard/base fields are auto-documented and the classification heads keep a checkpoint to fall back on. Verified: defaults, override, save/load round-trip, real-config load (biohub/ESMC-300M), and the 92 esmc model tests still pass; make typing + check_docstrings + check_config_attributes clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmc/configuration_esmc.py | 88 +++++++------------ 1 file changed, 31 insertions(+), 57 deletions(-) diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py index 070c93f4cf7d..64366e41d5b1 100644 --- a/src/transformers/models/esmc/configuration_esmc.py +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -13,49 +13,38 @@ # limitations under the License. """ESMC model configuration.""" -from ...configuration_utils import PretrainedConfig # type: ignore[import] +from huggingface_hub.dataclasses import strict +from ...configuration_utils import PreTrainedConfig # type: ignore[import] +from ...utils import auto_docstring -class ESMCConfig(PretrainedConfig): - """ - This is the configuration class to store the configuration of a [`ESMCModel`]. It is used to - instantiate an ESMC model according to the specified arguments, defining the model architecture. - Instantiating a configuration with the defaults will yield a similar configuration to that of the - [Biohub/ESMC-600M-2024-12](https://huggingface.co/Biohub/ESMC-600M-2024-12) architecture. - - Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model - outputs. Read the documentation from [`PretrainedConfig`] for more information. - Args: - vocab_size (`int`, *optional*, defaults to 64): - Vocabulary size of the ESMC model. Defines the number of different amino acid tokens that - can be represented by the ``input_ids`` passed to [`ESMCModel`]. - d_model (`int`, *optional*, defaults to 2560): - Dimensionality of the encoder layers and the pooler layer. - n_heads (`int`, *optional*, defaults to 40): - Number of attention heads for each attention layer in the Transformer encoder. - n_layers (`int`, *optional*, defaults to 80): - Number of hidden layers in the Transformer encoder. - pad_token_id (`int`, *optional*, defaults to 1): - Index of the padding token in the vocabulary (``""``). - mask_token_id (`int`, *optional*, defaults to 32): - Index of the mask token in the vocabulary (``""``), used for masked language modelling. - initializer_range (`float`, *optional*, defaults to 0.02): - The standard deviation of the truncated normal initialiser for weight matrix initialisation. - classifier_dropout (`float`, *optional*, defaults to 0.1): - Dropout ratio for the classification head. - rope_theta (`float`, *optional*, defaults to 10000.0): - The base period of the rotary position embeddings (RoPE). +@auto_docstring(checkpoint="biohub/ESMC-600M") +@strict +class ESMCConfig(PreTrainedConfig): + r""" + d_model (`int`, *optional*, defaults to 2560): + Dimensionality of the encoder layers and the pooler layer. + n_heads (`int`, *optional*, defaults to 40): + Number of attention heads for each attention layer in the Transformer encoder. + n_layers (`int`, *optional*, defaults to 80): + Number of hidden layers in the Transformer encoder. + mask_token_id (`int`, *optional*, defaults to 32): + Index of the mask token in the vocabulary (``""``), used for masked language modelling. + classifier_dropout (`float`, *optional*, defaults to 0.1): + Dropout ratio for the classification head. + rope_theta (`float`, *optional*, defaults to 10000.0): + The base period of the rotary position embeddings (RoPE). Examples: ```python >>> from transformers import ESMCConfig, ESMCModel - >>> # Initializing an ESMC EvolutionaryScale/esmc-600m-2024-12 style configuration + >>> # Initializing an ESMC biohub/ESMC-600M style configuration >>> configuration = ESMCConfig() - >>> # Initializing a model (with random weights) from the EvolutionaryScale/esmc-600m-2024-12 style configuration + >>> # Initializing a model (with random weights) from the configuration >>> model = ESMCModel(configuration) >>> # Accessing the model configuration @@ -65,31 +54,16 @@ class ESMCConfig(PretrainedConfig): model_type = "esmc" - def __init__( - self, - vocab_size: int = 64, - d_model: int = 2560, - n_heads: int = 40, - n_layers: int = 80, - pad_token_id: int = 1, - mask_token_id: int = 32, - initializer_range: float = 0.02, - classifier_dropout: float = 0.1, - rope_theta: float = 10000.0, - **kwargs, - ): - super().__init__( - pad_token_id=pad_token_id, mask_token_id=mask_token_id, **kwargs - ) - - self.vocab_size = vocab_size - self.d_model = d_model - self.n_heads = n_heads - self.n_layers = n_layers - self.initializer_range = initializer_range - self.classifier_dropout = classifier_dropout - self.rope_theta = rope_theta - self.tie_word_embeddings = False + vocab_size: int | None = 64 + d_model: int | None = 2560 + n_heads: int | None = 40 + n_layers: int | None = 80 + pad_token_id: int | None = 1 + mask_token_id: int | None = 32 + initializer_range: float | None = 0.02 + classifier_dropout: float | None = 0.1 + rope_theta: float | None = 10000.0 + tie_word_embeddings: bool | None = False __all__ = ["ESMCConfig"] From da35ca4d66c070ec6d8c16b996b0587dec781e35 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jun 2026 23:04:27 +0100 Subject: [PATCH 24/70] Fix ESMFold2 integration test: ubiquitin + correct 0-1 pLDDT/pTM scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @slow integration test used a poorly-folding sequence and asserted pLDDT on a 0-100 scale, but this model emits pLDDT/pTM on a 0-1 scale (_categorical_mean(..., start=0, end=1)) — so the old `mean > 50` assertion could never pass. Switch to ubiquitin (PDB 1UBQ, a textbook well-folding 76-mer), draw 8 diffusion samples and assert on the best one (these folders rank N samples and return the best): best pLDDT > 0.7 and best pTM > 0.6. Validated end-to-end on real weights (biohub/ESMFold2 + the 6B ESMC backbone, CPU/fp32 since this box's GPU can't JIT-compile): weights load with 0 missing/ unexpected keys, and ubiquitin folds to best pLDDT ~0.80 / pTM ~0.74, GB1 to ~0.85 / ~0.78 — confirming the port produces correct, confident structures. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/test_modeling_esmfold2.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index 106b48c0cc72..07db730ba01f 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -199,14 +199,23 @@ def test_inference_protein_folding(self): # from_pretrained auto-loads the ESMC backbone (load_esmc=True by default). model = ESMFold2Model.from_pretrained("biohub/ESMFold2").to(torch_device).eval() - seq = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKR" + # Ubiquitin (PDB 1UBQ), a textbook well-folding 76-residue domain. These + # diffusion folders draw several samples and the best-ranked is the + # prediction, so assert on the best of N. + seq = "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG" + torch.manual_seed(0) with torch.no_grad(): - output = model.infer_protein(seq, num_diffusion_samples=1) + output = model.infer_protein(seq, num_diffusion_samples=8, num_sampling_steps=68) coords = output["sample_atom_coords"] - plddt = output["plddt"] self.assertEqual(coords.shape[-1], 3) self.assertTrue(torch.isfinite(coords).all()) - # pLDDT is a 0-100 confidence; a real fold of this sequence should be confident. - self.assertTrue((plddt >= 0).all() and (plddt <= 100).all()) - self.assertGreater(plddt.mean().item(), 50.0) + + # pLDDT and pTM are on a 0-1 scale in this model; ESMFold2 folds ubiquitin + # confidently (CPU-fp32 reference: best pLDDT ~0.80, best pTM ~0.74). + plddt = output["plddt"].float() # [num_samples, n_res] + ptm = output["ptm"].float() # [num_samples] + best_plddt = plddt.mean(dim=1).max().item() + best_ptm = ptm.max().item() + self.assertGreater(best_plddt, 0.7) + self.assertGreater(best_ptm, 0.6) From 3e03361becb044245dff3cedf100ec05805324c1 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jun 2026 14:00:30 +0100 Subject: [PATCH 25/70] Make ESMFold2 dtype-honest: drop in-model autocast, support from_pretrained(dtype=) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESMFold2 was the only ported model relying on in-model `torch.amp.autocast` (`use_amp = device.type=="cuda"`) to (a) run matmuls in bf16 and (b) keep norms/softmax in fp32. That made it GPU-only for bf16 and silently broke CPU and `from_pretrained(dtype=bfloat16)`. Replace it with the standard Transformers idiom — the model runs in its loaded dtype, with numerically-sensitive ops explicitly pinned to fp32: - Add an fp32-pinned `LayerNorm(nn.LayerNorm)` (computes in fp32, returns the input dtype, keeps fp32 weights under `from_pretrained(dtype=bf16)`) and use it for every `nn.LayerNorm`; the affine-free `RMSNorm`/`F.rms_norm` stay in the activation dtype (they ran bf16 under autocast too). Pin the remaining softmaxes with `dtype=torch.float32`. - Remove the three in-model autocast regions (trunk, confidence trunk, ESMC `_lm_precision_context`) and convert the Kabsch/SVD `enabled=False` islands to plain `.float()`. - The model interleaves fp32 islands (geometry, coords, one-hot/continuous input features, the fp32-accumulated confidence pair, the fp32 triangular contract) with bf16 compute, which autocast used to bridge; add explicit `.to(dtype)` casts at each island→matmul boundary (atom encoder, inputs embedder, rel_pos, token_bonds, distogram head, MSA embed, diffusion s/z/coords conditioning, confidence pair). Also align the ESMC hidden states to the LM projection dtype, which additionally lets a bf16 backbone feed an fp32 trunk (no more `esmc_precision="fp32"` requirement on CPU). Validated vs the autocast baseline (ubiquitin, seed 0): GPU bf16 0.798/0.737, GPU fp32 0.800/0.740, CPU fp32 0.801/0.740 — all match 0.80/0.74. CPU bf16 runs correctly too (its matmul accumulates in fp32, ~4e-3 rel-err), just slower. make typing / check_docstrings / check_config_attributes / ruff clean; 7 tests + the @slow GPU integration test (now bf16) pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/modeling_esmfold2.py | 228 +++++++++--------- .../esmfold2/modeling_esmfold2_common.py | 117 +++++---- .../models/esmfold2/test_modeling_esmfold2.py | 5 +- 3 files changed, 184 insertions(+), 166 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index d63bced83f15..45b0938217f4 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -4,7 +4,7 @@ from transformers import ESMFold2Model - model = ESMFold2Model.from_pretrained("biohub/ESMFold2").cuda().eval() + model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() open("ubq.pdb", "w").write(model.infer_protein_as_pdb("MQIFVKTLTGKT...")) For multi-chain / ligand / MSA inputs see ``ESMFold2InputBuilder`` in the @@ -12,7 +12,6 @@ """ import math -from contextlib import contextmanager from typing import cast import torch @@ -30,6 +29,7 @@ FoldingTrunk, InputsEmbedder, LanguageModelShim, + LayerNorm, MSAPairWeightedAveraging, OuterProductMean, ResIdxAsymIdSymIdEntityIdEncoding, @@ -61,7 +61,7 @@ class PairTransition(nn.Module): def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: super().__init__() - self.norm = nn.LayerNorm(d_model) + self.norm = LayerNorm(d_model) self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE @@ -95,7 +95,7 @@ def __init__(self, config: "ESMFold2Config") -> None: self.register_buffer("boundaries", boundaries) self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) - self.s_norm = nn.LayerNorm(d_single) + self.s_norm = LayerNorm(d_single) self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) @@ -103,8 +103,8 @@ def __init__(self, config: "ESMFold2Config") -> None: self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) - self.s_inputs_norm = nn.LayerNorm(d_inputs) - self.z_norm = nn.LayerNorm(d_pair) + self.s_inputs_norm = LayerNorm(d_inputs) + self.z_norm = LayerNorm(d_pair) self.row_attention_pooling = RowAttentionPooling(d_pair=d_pair, d_single=d_single) @@ -112,17 +112,17 @@ def __init__(self, config: "ESMFold2Config") -> None: self.folding_trunk = FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) # Heads. - self.plddt_ln = nn.LayerNorm(d_single) + self.plddt_ln = LayerNorm(d_single) max_atoms_per_token = 23 self.plddt_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)) - self.pae_ln = nn.LayerNorm(d_pair) + self.pae_ln = LayerNorm(d_pair) self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) - self.pde_ln = nn.LayerNorm(d_pair) + self.pde_ln = LayerNorm(d_pair) self.pde_head = nn.Linear(d_pair, ch.num_pde_bins, bias=False) - self.resolved_ln = nn.LayerNorm(d_single) + self.resolved_ln = LayerNorm(d_single) # 2 = resolved logits ([unresolved, resolved]). self.resolved_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, 2)) @@ -183,13 +183,13 @@ def forward( pair_mask = mask[:, :, None].float() * mask[:, None, :].float() - # FoldingTrunk handles the bf16 cast internally during inference so - # each block's fused trimul engages. In-place residual avoids an - # extra fp32 pair allocation. - with torch.amp.autocast("cuda", enabled=pair.is_cuda, dtype=torch.bfloat16): - pair_delta = self.folding_trunk(pair, pair_attention_mask=pair_mask) + # `pair` is fp32 here (built from the fp32 trunk output `z`); run the + # folding trunk in the model's compute dtype, then accumulate in fp32. + pair_delta = self.folding_trunk(pair.to(self.pae_head.weight.dtype), pair_attention_mask=pair_mask) pair.add_(pair_delta.float()) del pair_delta + # Accumulated in fp32; hand the downstream confidence heads the compute dtype. + pair = pair.to(self.pae_head.weight.dtype) single = self.row_attention_pooling(pair, mask) atom_mask_f = atom_mask_m.float() @@ -250,7 +250,7 @@ def forward( N_res = mask_f.sum(dim=-1, keepdim=True) d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2) - pae_probs = F.softmax(pae_logits, dim=-1) + pae_probs = F.softmax(pae_logits, dim=-1, dtype=torch.float32) tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1) pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) @@ -296,13 +296,6 @@ def _inverse_softplus(value: float) -> float: return value + math.log(-math.expm1(-value)) -@contextmanager -def _lm_precision_context(): - """bf16 autocast around the LM (ESMC backbone) forward.""" - with torch.autocast(device_type="cuda", dtype=torch.bfloat16): - yield - - class ESMFold2Model(PreTrainedModel): """ESMFold2 — all-atom structure prediction with an ESMC PLM backbone. @@ -358,7 +351,7 @@ def __init__(self, config: ESMFold2Config) -> None: else: self.lm_encoder = None - self.parcae_input_norm = nn.LayerNorm(d_pair) + self.parcae_input_norm = LayerNorm(d_pair) self.parcae_log_a = nn.Parameter(torch.zeros(d_pair)) parcae_decay_init = math.sqrt(1.0 / 5.0) parcae_delta_init = -math.log(parcae_decay_init) @@ -482,16 +475,15 @@ def _compute_lm_hidden_states( tok_mask: Tensor, ) -> Tensor: assert self._esmc is not None - with _lm_precision_context(): - return compute_lm_hidden_states( - self._esmc, - input_ids, - asym_id, - residue_index, - mol_type, - tok_mask, - pad_to_multiple=None, - ) + return compute_lm_hidden_states( + self._esmc, + input_ids, + asym_id, + residue_index, + mol_type, + tok_mask, + pad_to_multiple=None, + ) def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: delta = F.softplus(self.parcae_log_delta) @@ -627,97 +619,93 @@ def forward( ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(-1).unsqueeze(-1) atom_to_token = atom_to_token * atm_mask.long() - use_amp = ref_pos.device.type == "cuda" - with torch.amp.autocast("cuda", enabled=use_amp, dtype=torch.bfloat16): - x_inputs = self.inputs_embedder( - aatype=res_type_oh, - profile=profile.float(), - deletion_mean=deletion_mean.float(), - ref_pos=ref_pos, - atom_attention_mask=atm_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element_oh, - ref_atom_name_chars=ref_atom_name_chars_oh, - atom_to_token=atom_to_token, - ) + x_inputs = self.inputs_embedder( + aatype=res_type_oh, + profile=profile.float(), + deletion_mean=deletion_mean.float(), + ref_pos=ref_pos, + atom_attention_mask=atm_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element_oh, + ref_atom_name_chars=ref_atom_name_chars_oh, + atom_to_token=atom_to_token, + ) - z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(x_inputs).unsqueeze(1) + z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(x_inputs).unsqueeze(1) - relative_position_encoding = self.rel_pos( - residue_index=residue_index, - asym_id=asym_id, - sym_id=sym_id, - entity_id=entity_id, - token_index=token_index, + relative_position_encoding = self.rel_pos( + residue_index=residue_index, + asym_id=asym_id, + sym_id=sym_id, + entity_id=entity_id, + token_index=token_index, + ) + token_bonds_encoding = self.token_bonds(token_bonds.to(self.token_bonds.weight.dtype)) + z_init = z_init + relative_position_encoding + token_bonds_encoding + + if lm_hidden_states is None and input_ids is not None and self._esmc is not None: + lm_hidden_states = self._compute_lm_hidden_states(input_ids, asym_id, residue_index, mol_type, tok_mask) + lm_z: Tensor | None = None + if lm_hidden_states is not None: + lm_z = self.language_model(lm_hidden_states.detach()) + del lm_hidden_states + + pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() + + z = self._init_pair_state(z_init) + + a, b = self._discretized_dynamics() + a = a.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) + b_mat = b.to(device=z.device, dtype=z.dtype) + + _msa_kwargs: dict | None = None + if self.msa_encoder is not None and msa is not None: + B_msa, M, L_msa = msa.shape + msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES).float() + msa_attn = ( + msa_attention_mask.permute(0, 2, 1).float() + if msa_attention_mask is not None + else tok_mask[:, :, None].expand(-1, -1, M).float() ) - token_bonds_encoding = self.token_bonds(token_bonds.float()) - z_init = z_init + relative_position_encoding + token_bonds_encoding - - if lm_hidden_states is None and input_ids is not None and self._esmc is not None: - lm_hidden_states = self._compute_lm_hidden_states( - input_ids, asym_id, residue_index, mol_type, tok_mask - ) - lm_z: Tensor | None = None - if lm_hidden_states is not None: - lm_z = self.language_model(lm_hidden_states.detach()) - del lm_hidden_states - - pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() - - z = self._init_pair_state(z_init) - - a, b = self._discretized_dynamics() - a = a.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) - b_mat = b.to(device=z.device, dtype=z.dtype) - - _msa_kwargs: dict | None = None - if self.msa_encoder is not None and msa is not None: - B_msa, M, L_msa = msa.shape - msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES).float() - msa_attn = ( - msa_attention_mask.permute(0, 2, 1).float() - if msa_attention_mask is not None - else tok_mask[:, :, None].expand(-1, -1, M).float() - ) - # Bias-free MSAEncoder.embed requires zeroed padding. - msa_oh = msa_oh * msa_attn.unsqueeze(-1) - hd = ( - has_deletion.permute(0, 2, 1).float() - if has_deletion is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - dv = ( - deletion_value.permute(0, 2, 1).float() - if deletion_value is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - _msa_kwargs = { - "x_inputs": x_inputs, - "msa_oh": msa_oh, - "has_deletion": hd, - "deletion_value": dv, - "msa_attention_mask": msa_attn, - } - - # Method call (not inline loop) frees per-iter L²×c_z locals. - z = self._run_one_loop( - z=z, - z_init=z_init, - lm_z=lm_z, - _msa_kwargs=_msa_kwargs, - pair_mask=pair_mask, - a=a, - b_mat=b_mat, - total_steps=total_steps, + # Bias-free MSAEncoder.embed requires zeroed padding. + msa_oh = msa_oh * msa_attn.unsqueeze(-1) + hd = ( + has_deletion.permute(0, 2, 1).float() + if has_deletion is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + dv = ( + deletion_value.permute(0, 2, 1).float() + if deletion_value is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) ) - del z_init, lm_z, _msa_kwargs, a, b_mat + _msa_kwargs = { + "x_inputs": x_inputs, + "msa_oh": msa_oh, + "has_deletion": hd, + "deletion_value": dv, + "msa_attention_mask": msa_attn, + } + + # Method call (not inline loop) frees per-iter L²×c_z locals. + z = self._run_one_loop( + z=z, + z_init=z_init, + lm_z=lm_z, + _msa_kwargs=_msa_kwargs, + pair_mask=pair_mask, + a=a, + b_mat=b_mat, + total_steps=total_steps, + ) + del z_init, lm_z, _msa_kwargs, a, b_mat - z = self.parcae_readout(z) - z = self.parcae_coda(z, pair_attention_mask=pair_mask) + z = self.parcae_readout(z) + z = self.parcae_coda(z, pair_attention_mask=pair_mask) - z = z.float() - distogram_logits = self.distogram_head(z + z.transpose(-2, -3)) + z = z.float() + distogram_logits = self.distogram_head((z + z.transpose(-2, -3)).to(self.distogram_head.weight.dtype)) structure_output = self.structure_head.sample( z_trunk=z, @@ -881,7 +869,7 @@ def forward( ) -> Tensor: # All inputs are pre-transposed to [B, L, M, ...] before calling. m_feat = torch.cat([msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1) - m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2) + m = self.embed(m_feat.to(self.embed.weight.dtype)) + self.project_inputs(x_inputs).unsqueeze(2) tok_mask = msa_attention_mask[:, :, 0].bool() pair_attention_mask = tok_mask.unsqueeze(2) & tok_mask.unsqueeze(1) for block in self.blocks: diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index f9c46add76e5..49a2172ed16b 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -45,6 +45,26 @@ from .configuration_esmfold2 import ESMFold2Config +class LayerNorm(nn.LayerNorm): + """LayerNorm pinned to float32. + + Normalization statistics are numerically sensitive, so this computes in + float32 and keeps its weights in float32 even when the model is loaded in a + lower precision (the explicit ``dtype`` overrides ``from_pretrained(dtype=...)``). + Inputs/outputs keep their original dtype, so it drops into a bf16 model + transparently. This is the standard Transformers idiom (matmuls run in the + model dtype; norms stay fp32) and replaces the model's former reliance on + autocast to keep norms in fp32. + """ + + def __init__(self, *args, **kwargs): + kwargs.setdefault("dtype", torch.float32) + super().__init__(*args, **kwargs) + + def forward(self, x: Tensor) -> Tensor: + return super().forward(x.float()).to(x.dtype) + + class DropoutResidual(nn.Module): """``residual + dropout(delta)`` with row/col-shared dropout.""" @@ -201,7 +221,7 @@ class TransitionLayer(nn.Module): def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: super().__init__() hidden = n * d_model - self.norm = nn.LayerNorm(d_model, eps=eps) + self.norm = LayerNorm(d_model, eps=eps) self.a_proj = nn.Linear(d_model, hidden, bias=False) self.b_proj = nn.Linear(d_model, hidden, bias=False) self.out_proj = nn.Linear(hidden, d_model, bias=False) @@ -231,8 +251,8 @@ def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: self.s_shift = nn.Linear(d_cond, d_model, bias=False) def forward(self, a: Tensor, s: Tensor) -> Tensor: - a_norm = F.layer_norm(a, (self.d_model,), None, None, self.eps) - s_norm = F.layer_norm(s, (self.d_cond,), self.s_scale, None, self.eps) + a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps).to(a.dtype) + s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) return torch.sigmoid(self.s_gate(s_norm)) * a_norm + self.s_shift(s_norm) @@ -698,7 +718,7 @@ def __init__( self.structure_prediction = structure_prediction self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) - self.atom_norm = nn.LayerNorm(d_atom) + self.atom_norm = LayerNorm(d_atom) if structure_prediction: self.coords_linear = nn.Linear(6, d_atom, bias=False) @@ -758,7 +778,7 @@ def forward( ], dim=-1, ) - c_base = self.atom_norm(self.atom_linear(atom_feats)) + c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype))) cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) cos = cos.repeat_interleave(num_diffusion_samples, 0) sin = sin.repeat_interleave(num_diffusion_samples, 0) @@ -790,7 +810,7 @@ def forward( if pred_r1 is None: pred_r1 = torch.zeros_like(r_l) r_input = torch.cat([r_l, pred_r1], dim=-1) - r_to_q = self.coords_linear(r_input) + r_to_q = self.coords_linear(r_input.to(self.coords_linear.weight.dtype)) q = q + r_to_q c = c.repeat_interleave(num_diffusion_samples, 0) @@ -853,7 +873,7 @@ def __init__( uid_rope_base_frequency=uid_rope_base_frequency, ) - self.norm = nn.LayerNorm(d_atom) + self.norm = LayerNorm(d_atom) self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) def forward( @@ -919,7 +939,7 @@ def __init__( nn.init.zeros_(self.out_gate.weight) nn.init.constant_(self.out_gate.bias, -2.0) else: - self.pre_norm = nn.LayerNorm(d_model, eps=1e-5) + self.pre_norm = LayerNorm(d_model, eps=1e-5) self.q_proj = nn.Linear(d_model, d_model, bias=True) self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) @@ -927,7 +947,7 @@ def __init__( self.out_proj = nn.Linear(d_model, d_model, bias=False) if d_pair > 0: - self.pair_norm = nn.LayerNorm(d_pair, eps=1e-5) + self.pair_norm = LayerNorm(d_pair, eps=1e-5) self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) def forward( @@ -975,7 +995,7 @@ def forward( mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) logits = logits + mask_bias.to(dtype=logits.dtype) - attn = torch.softmax(logits, dim=-2).to(dtype=v.dtype) + attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) ctx = g * ctx out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) @@ -1010,7 +1030,7 @@ def __init__( nn.init.zeros_(self.output_gate.weight) nn.init.constant_(self.output_gate.bias, -2.0) else: - self.pre_norm = nn.LayerNorm(d_model, eps=1e-5) + self.pre_norm = LayerNorm(d_model, eps=1e-5) self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) self.lin_out = nn.Linear(hidden, d_model, bias=False) @@ -1126,16 +1146,16 @@ def __init__( self.c_s = c_s self.c_s_inputs = c_s_inputs - self.z_input_norm = nn.LayerNorm(2 * c_z, eps=layer_norm_eps) + self.z_input_norm = LayerNorm(2 * c_z, eps=layer_norm_eps) self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) self.z_transitions = nn.ModuleList( [TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] ) - self.s_input_norm = nn.LayerNorm(c_s_inputs, eps=layer_norm_eps) + self.s_input_norm = LayerNorm(c_s_inputs, eps=layer_norm_eps) self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) self.fourier = FourierEmbedding(fourier_dim) - self.noise_norm = nn.LayerNorm(fourier_dim, eps=layer_norm_eps) + self.noise_norm = LayerNorm(fourier_dim, eps=layer_norm_eps) self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) self.s_transitions = nn.ModuleList( [TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] @@ -1162,10 +1182,11 @@ def forward( else: z_rel = relative_position_encoding.to(dtype=torch.float32) z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) - z = self.z_proj(self.z_input_norm(z)) - with torch.autocast(device_type="cuda", dtype=torch.bfloat16): - for block in self.z_transitions: - z = z + block(z) + # The relpos/coords conditioning is fp32; z_input_norm keeps it fp32, + # then we hand off to z_proj in the model's compute dtype. + z = self.z_proj(self.z_input_norm(z).to(self.z_proj.weight.dtype)) + for block in self.z_transitions: + z = z + block(z) if inference_cache is not None: inference_cache["z"] = z @@ -1174,7 +1195,7 @@ def forward( if s_inputs_eff.shape[0] != target_batch: s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) - s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32))) + s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype)) # Noise embedding t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) @@ -1275,8 +1296,8 @@ def __init__( use_conditioning=True, ) - self.s_step_norm = nn.LayerNorm(c_token) - self.token_norm = nn.LayerNorm(c_token) + self.s_step_norm = LayerNorm(c_token) + self.token_norm = LayerNorm(c_token) def forward( self, @@ -1635,9 +1656,9 @@ def sample( if request_atom_repr: diff_atom_intermediates = dm_out.get("atom_intermediates") - # Reverse diffusion alignment (Kabsch) - with torch.autocast(device_type="cuda", enabled=False): - x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) + # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts + # to fp32 internally for the SVD/det. + x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) x_noisy = x_noisy.to(dtype=x_denoised.dtype) # ODE/SDE step @@ -1647,13 +1668,12 @@ def sample( # Denoising early-exit: stop when consecutive predictions converge if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: - with torch.autocast(device_type="cuda", enabled=False): - aligned = self._weighted_rigid_align( - x_denoised_prev.float(), - x_denoised.float(), - atom_mask, - atom_mask, - ) + aligned = self._weighted_rigid_align( + x_denoised_prev.float(), + x_denoised.float(), + atom_mask, + atom_mask, + ) diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: @@ -1693,7 +1713,7 @@ def forward(self, z: Tensor, mask: Tensor) -> Tensor: torch.full_like(scores, -1e9), ) scores = scores + mask_bias - weights = F.softmax(scores, dim=-1) + weights = F.softmax(scores, dim=-1, dtype=torch.float32).to(scores.dtype) pooled = torch.einsum("bnm,bnmd->bnd", weights, z) return self.out_proj(pooled) @@ -1752,7 +1772,13 @@ def forward( ref_atom_name_chars=ref_atom_name_chars, atom_to_token=atom_to_token, ) - return torch.cat([a, aatype, profile, deletion_mean.unsqueeze(-1)], dim=-1) + # The continuous input features are fp32; fold them into the atom + # encoding's (compute) dtype so the single representation is one dtype. + dtype = a.dtype + return torch.cat( + [a, aatype.to(dtype), profile.to(dtype), deletion_mean.unsqueeze(-1).to(dtype)], + dim=-1, + ) # =========================================================================== @@ -1835,7 +1861,7 @@ def forward( dim=-1, ) - return self.embed(feats) + return self.embed(feats.to(self.embed.weight.dtype)) # =========================================================================== @@ -1881,8 +1907,8 @@ class LanguageModelShim(nn.Module): def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: super().__init__() - self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z)) - self.base_z_linear = nn.Sequential(nn.LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) + self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), LayerNorm(d_z)) + self.base_z_linear = nn.Sequential(LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: @@ -1896,6 +1922,9 @@ def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: Returns: [B, L, L, d_pair] pair representation. """ + # The ESMC backbone may be loaded at a different precision than the trunk + # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. + hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) lm_z = self.base_z_linear(hidden_states) # [B, L, 81, d_z] weights = self.base_z_combine.softmax(0) # [81] lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] @@ -2067,8 +2096,8 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None self.latent_channels = latent_channels self.flow = flow self._einsum_equation = self._FLOW_TO_EINSUM[flow] - self.norm_start = nn.LayerNorm(self.input_channels, eps=_EPS) - self.norm_mix = nn.LayerNorm(self.latent_channels, eps=_EPS) + self.norm_start = LayerNorm(self.input_channels, eps=_EPS) + self.norm_mix = LayerNorm(self.latent_channels, eps=_EPS) self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) @@ -2112,7 +2141,7 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) else: contracted = self._triangular_contract(left_stream, right_stream) - mixed = self.proj_emit(self.norm_mix(contracted)) + mixed = self.proj_emit(self.norm_mix(contracted).to(self.proj_emit.weight.dtype)) output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) return mixed * output_gate @@ -2142,7 +2171,7 @@ class Transition(nn.Module): def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: super().__init__() - self.norm = nn.LayerNorm(d_model) + self.norm = LayerNorm(d_model) self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. self._chunk_size: int | None = 64 @@ -2235,7 +2264,7 @@ def __init__( super().__init__() self.d_hidden = d_hidden self.divide_outer_before_proj = divide_outer_before_proj - self.norm = nn.LayerNorm(d_msa) + self.norm = LayerNorm(d_msa) self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. @@ -2276,8 +2305,8 @@ def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = super().__init__() self.n_heads = n_heads self.head_width = head_width - self.norm_single = nn.LayerNorm(d_msa) - self.compute_bias = nn.Sequential(nn.LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) + self.norm_single = LayerNorm(d_msa) + self.compute_bias = nn.Sequential(LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) @@ -2297,7 +2326,7 @@ def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tens msa_normed = self.norm_single(msa_repr) bias = self.compute_bias(pair_repr) # [B, L, L, n_heads] bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) - attn = torch.softmax(bias, dim=-2) # softmax over j + attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j v = self.Wv(msa_normed).reshape(B, L, M, h, dh) gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index 07db730ba01f..67c2440126c3 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -196,8 +196,9 @@ class ESMFold2IntegrationTest(TestCasePlus): @slow @require_torch_accelerator def test_inference_protein_folding(self): - # from_pretrained auto-loads the ESMC backbone (load_esmc=True by default). - model = ESMFold2Model.from_pretrained("biohub/ESMFold2").to(torch_device).eval() + # bf16 is the intended inference regime; from_pretrained auto-loads the + # ESMC backbone (load_esmc=True by default). + model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).to(torch_device).eval() # Ubiquitin (PDB 1UBQ), a textbook well-folding 76-residue domain. These # diffusion folders draw several samples and the best-ranked is the From 99b0b499a206ebaa7e1f618171a6fd729ef1b4c6 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jun 2026 14:27:46 +0100 Subject: [PATCH 26/70] Apply make fix-repo sweep + bf16 ESMFold2 usage example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make fix-repo` adds the `add_dates` "contributed on …" stamp to the esmc/esmfold2 doc pages and ruff-reformats a few esmc files (line-length only, no logic change). Also update the esmfold2.md usage example to the recommended `dtype=torch.bfloat16`. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/source/en/model_doc/esmc.md | 1 + docs/source/en/model_doc/esmfold2.md | 6 +++++- src/transformers/models/esmc/__init__.py | 4 +--- src/transformers/models/esmc/tokenization_esmc.py | 4 +--- tests/models/esmc/test_modeling_esmc.py | 4 +--- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 1d3c3de42826..792ba47df906 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,6 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> +*This model was contributed to Hugging Face Transformers on 2026-06-04.* # ESMC diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 9df1c85cf042..7f0626512f38 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,6 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> +*This model was contributed to Hugging Face Transformers on 2026-06-04.* # ESMFold2 @@ -34,10 +35,13 @@ The model checkpoint is available on the Hugging Face Hub at ## Usage example ```python +import torch + from transformers import ESMFold2Model # Loading the model also downloads and attaches the ESMC backbone (config.esmc_id). -model = ESMFold2Model.from_pretrained("biohub/ESMFold2").cuda().eval() +# bf16 is the recommended inference precision. +model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() pdb_string = model.infer_protein_as_pdb("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ") with open("prediction.pdb", "w") as f: diff --git a/src/transformers/models/esmc/__init__.py b/src/transformers/models/esmc/__init__.py index 750ed540a2c7..30b5b38c86d7 100644 --- a/src/transformers/models/esmc/__init__.py +++ b/src/transformers/models/esmc/__init__.py @@ -25,6 +25,4 @@ import sys _file = globals()["__file__"] - sys.modules[__name__] = _LazyModule( - __name__, _file, define_import_structure(_file), module_spec=__spec__ - ) + sys.modules[__name__] = _LazyModule(__name__, _file, define_import_structure(_file), module_spec=__spec__) diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index 73fbe46aab57..3c1f65e8f21a 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -147,9 +147,7 @@ def __init__( # preserved during encode/decode and can be looked up easily. kwargs.setdefault("additional_special_tokens", []) if chain_break_token not in kwargs["additional_special_tokens"]: - kwargs["additional_special_tokens"] = list( - kwargs["additional_special_tokens"] - ) + [chain_break_token] + kwargs["additional_special_tokens"] = list(kwargs["additional_special_tokens"]) + [chain_break_token] # Keep reference before super().__init__ so properties below work. self._chain_break_token = chain_break_token diff --git a/tests/models/esmc/test_modeling_esmc.py b/tests/models/esmc/test_modeling_esmc.py index 40b6de49bf48..350af3bbe085 100644 --- a/tests/models/esmc/test_modeling_esmc.py +++ b/tests/models/esmc/test_modeling_esmc.py @@ -121,9 +121,7 @@ def create_and_check_for_sequence_classification( result = model(input_ids, attention_mask=input_mask, labels=sequence_labels) self.parent.assertEqual(result.logits.shape, (self.batch_size, self.num_labels)) - def create_and_check_for_token_classification( - self, config, input_ids, input_mask, sequence_labels, token_labels - ): + def create_and_check_for_token_classification(self, config, input_ids, input_mask, sequence_labels, token_labels): config.num_labels = self.num_labels model = ESMCForTokenClassification(config=config) model.to(torch_device) From 870be2caa2167cfc479f12cae944a9eef005fd5f Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jun 2026 14:27:46 +0100 Subject: [PATCH 27/70] Keep ESMFold2 sub-configs internal (drop their model_types) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sub-config model_types added earlier (for the ConfigTester composite test) made `make fix-repo` auto-register all 9 in CONFIG_MAPPING_NAMES, which then failed check_repo (a registered config must be importable from `transformers`, but these are intentionally not exported — `__all__ = ["ESMFold2Config"]`, like ESM exporting only EsmConfig). Rather than expose 9 internal architecture configs publicly (with the docs/docstring burden that implies), drop the sub-config model_types so they stay internal, and skip the composite "load each sub-config standalone from the parent dir" ConfigTester check, which doesn't apply to internal sub-configs. Serialization still round-trips via `sub_configs` + `__post_init__` (no model_type needed); config tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../models/esmfold2/configuration_esmfold2.py | 15 --------------- tests/models/esmfold2/test_modeling_esmfold2.py | 4 ++++ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 029530acf4fc..539a5b906b71 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -33,8 +33,6 @@ class MSAEncoderConfig(PreTrainedConfig): """Config for the optional MSA encoder module (Large MSA models only).""" - model_type = "esmfold2_msa_encoder" - enabled: bool | None = False d_msa: int | None = 128 d_hidden: int | None = 32 @@ -47,8 +45,6 @@ class MSAEncoderConfig(PreTrainedConfig): class ParcaeConfig(PreTrainedConfig): """Release-only config for the parcae diffusion-loop scheduler.""" - model_type = "esmfold2_parcae" - enabled: bool | None = True poisson_mean: float | None = 3.0 min_steps: int | None = 1 @@ -60,8 +56,6 @@ class ParcaeConfig(PreTrainedConfig): class LMEncoderConfig(PreTrainedConfig): """Release-only config for the LM-side pair encoder.""" - model_type = "esmfold2_lm_encoder" - enabled: bool | None = True n_layers: int | None = 4 lm_dropout: float | None = 0.25 @@ -72,8 +66,6 @@ class LMEncoderConfig(PreTrainedConfig): class AtomAttentionConfig(PreTrainedConfig): """Config for the SWA atom encoder/decoder with 3D RoPE.""" - model_type = "esmfold2_atom_attention" - d_atom: int | None = 128 d_token: int | None = 768 n_blocks: int | None = 3 @@ -90,8 +82,6 @@ class AtomAttentionConfig(PreTrainedConfig): class FoldingTrunkConfig(PreTrainedConfig): """Config for a pairwise folding trunk stack.""" - model_type = "esmfold2_folding_trunk" - n_layers: int | None = 24 n_heads: int | None = 8 dropout: float | None = 0.0 @@ -101,7 +91,6 @@ class FoldingTrunkConfig(PreTrainedConfig): class InputsEmbedderConfig(PreTrainedConfig): """Config for the inputs embedder (wraps the atom encoder).""" - model_type = "esmfold2_inputs_embedder" sub_configs = {"atom_encoder": AtomAttentionConfig} d_inputs: int | None = 451 @@ -119,8 +108,6 @@ def __post_init__(self, **kwargs): class DiffusionModuleConfig(PreTrainedConfig): """Config for the DiffusionModule.""" - model_type = "esmfold2_diffusion_module" - sigma_data: float | None = 16.0 c_atom: int | None = 128 c_token: int | None = 768 @@ -140,7 +127,6 @@ class DiffusionModuleConfig(PreTrainedConfig): class DiffusionStructureHeadConfig(PreTrainedConfig): """Config for the diffusion-based structure prediction head.""" - model_type = "esmfold2_structure_head" sub_configs = {"diffusion_module": DiffusionModuleConfig} diffusion_module: dict | DiffusionModuleConfig | None = None @@ -171,7 +157,6 @@ def __post_init__(self, **kwargs): class ConfidenceHeadConfig(PreTrainedConfig): """Config for the confidence prediction head.""" - model_type = "esmfold2_confidence_head" sub_configs = {"folding_trunk": FoldingTrunkConfig} enabled: bool | None = True diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index 67c2440126c3..c24757d16ac0 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -104,6 +104,10 @@ class ESMFold2ConfigTest(unittest.TestCase): def setUp(self): # ESMFold2Config is composite (sub_configs) with no vocab/hidden_size. self.config_tester = ConfigTester(self, config_class=ESMFold2Config, has_text_modality=False, num_loops=5) + # ESMFold2's sub-configs are internal architecture details (no model_type, + # not in the auto mappings), so the composite "load each sub-config + # standalone from the parent dir" check does not apply. + self.config_tester.create_and_test_config_from_and_save_pretrained_composite = lambda: None def test_config(self): self.config_tester.run_common_tests() From 203e73ed9cb48119238bb90fe9413d04c65a8118 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Jun 2026 15:08:23 +0100 Subject: [PATCH 28/70] Add kernel, update docs --- docs/source/en/model_doc/esmfold2.md | 20 +++++++++++++++++++ .../esmfold2/modeling_esmfold2_common.py | 14 ++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 7f0626512f38..709d760e4371 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -51,6 +51,26 @@ with open("prediction.pdb", "w") as f: `infer_protein` returns the raw outputs (atom coordinates, distogram logits and confidence metrics) as a dictionary if you need them instead of a PDB string. Generation is stochastic — set a manual seed for reproducible structures. +## Faster inference with a fused kernel + +The folding trunk's dominant cost is the triangle-multiplication update. Passing `use_kernels=True` to +[`~PreTrainedModel.from_pretrained`] swaps it for a fused Triton kernel loaded from the Hub via the +[`kernels`](https://github.com/huggingface/kernels) library, leaving the prediction unchanged. It is inference-only and +CUDA-only; on CPU or without the kernel installed the model transparently falls back to the pure-PyTorch implementation. +Make sure the model is on a CUDA device when kernelization happens (e.g. with `device_map`). + +```python +import torch + +from transformers import ESMFold2Model + +model = ESMFold2Model.from_pretrained( + "biohub/ESMFold2", dtype=torch.bfloat16, device_map="cuda", use_kernels=True +).eval() + +pdb_string = model.infer_protein_as_pdb("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ") +``` + ## ESMFold2Config [[autodoc]] ESMFold2Config diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 49a2172ed16b..0a6b10b84635 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -41,6 +41,7 @@ pad_input = None # type: ignore[assignment] FLASH_ATTN_AVAILABLE = False +from ...integrations import use_kernel_forward_from_hub from ...modeling_utils import ALL_ATTENTION_FUNCTIONS # type: ignore[import] from .configuration_esmfold2 import ESMFold2Config @@ -2081,8 +2082,19 @@ def compute_lm_hidden_states( # =========================================================================== # TriangleMultiplicativeUpdate # =========================================================================== +@use_kernel_forward_from_hub("ESMFold2TriangleMultiplication") class TriangleMultiplicativeBlock(nn.Module): - """Triangle multiplicative update block with gated signal routing.""" + """Triangle multiplicative update block with gated signal routing. + + The O(N^3) triangular contraction below is the trunk's dominant cost. Loading + with ``ESMFold2Model.from_pretrained(..., device_map="cuda", use_kernels=True)`` + (CUDA + inference) swaps the whole block forward for a fused Triton kernel from + the Hub (see the ``hub_kernels`` mapping); the pure-PyTorch ``forward`` here stays + as the reference/fallback. The kernel reads this module's parameters + (``norm_start``/``norm_mix``/``proj_bundle``/``proj_emit``/``proj_gate``) and + matches ``forward``'s ``(pair_grid, visibility)`` signature, returning the + residual-free delta. + """ _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} _VALID_FLOWS = ("outgoing", "incoming") From 1520d007c42bb8e012a6aac5cee6003b12954c97 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Jun 2026 16:16:10 +0100 Subject: [PATCH 29/70] Some cleanup --- .../models/esmfold2/modeling_esmfold2.py | 70 ++--- .../esmfold2/modeling_esmfold2_common.py | 248 +++++++----------- 2 files changed, 108 insertions(+), 210 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 45b0938217f4..b4b550abe4eb 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -12,14 +12,13 @@ """ import math -from typing import cast import torch import torch.nn as nn import torch.nn.functional as F from torch import Tensor -from ...modeling_utils import PreTrainedModel # type: ignore[import] +from ...modeling_utils import PreTrainedModel from .configuration_esmfold2 import ESMFold2Config from .modeling_esmfold2_common import ( CHAR_VOCAB_SIZE, @@ -29,13 +28,12 @@ FoldingTrunk, InputsEmbedder, LanguageModelShim, - LayerNorm, MSAPairWeightedAveraging, OuterProductMean, ResIdxAsymIdSymIdEntityIdEncoding, RowAttentionPooling, SWA3DRoPEAttention, - SwiGLUMLP, + Transition, TriangleMultiplicativeUpdate, _categorical_mean, _compute_intra_token_idx, @@ -48,36 +46,6 @@ _EPS = 1e-6 _NONPOLYMER_ID = 4 -# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory -# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; -# chunk=64 leaves headroom for the largest foldbench targets). Override via -# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for -# short L but OOM-prone past ~600). -_DEFAULT_CHUNK_SIZE = 64 - - -class PairTransition(nn.Module): - """LayerNorm + SwiGLU feed-forward residual block on the pair representation.""" - - def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: - super().__init__() - self.norm = LayerNorm(d_model) - self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) - self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def forward(self, x: Tensor) -> Tensor: - if self._chunk_size is None or x.shape[1] <= self._chunk_size: - return self.ffn(self.norm(x)) - out: list[Tensor] = [] - for s in range(0, x.shape[1], self._chunk_size): - e = min(s + self._chunk_size, x.shape[1]) - sl = x[:, s:e] - out.append(self.ffn(self.norm(sl))) - return torch.cat(out, dim=1) - class ConfidenceHead(nn.Module): """Predicts pLDDT, PAE, PDE, resolved-atom probability and distogram bins.""" @@ -95,7 +63,7 @@ def __init__(self, config: "ESMFold2Config") -> None: self.register_buffer("boundaries", boundaries) self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) - self.s_norm = LayerNorm(d_single) + self.s_norm = nn.LayerNorm(d_single, dtype=torch.float32) self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) @@ -103,8 +71,8 @@ def __init__(self, config: "ESMFold2Config") -> None: self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) - self.s_inputs_norm = LayerNorm(d_inputs) - self.z_norm = LayerNorm(d_pair) + self.s_inputs_norm = nn.LayerNorm(d_inputs, dtype=torch.float32) + self.z_norm = nn.LayerNorm(d_pair, dtype=torch.float32) self.row_attention_pooling = RowAttentionPooling(d_pair=d_pair, d_single=d_single) @@ -112,17 +80,17 @@ def __init__(self, config: "ESMFold2Config") -> None: self.folding_trunk = FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) # Heads. - self.plddt_ln = LayerNorm(d_single) + self.plddt_ln = nn.LayerNorm(d_single, dtype=torch.float32) max_atoms_per_token = 23 self.plddt_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)) - self.pae_ln = LayerNorm(d_pair) + self.pae_ln = nn.LayerNorm(d_pair, dtype=torch.float32) self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) - self.pde_ln = LayerNorm(d_pair) + self.pde_ln = nn.LayerNorm(d_pair, dtype=torch.float32) self.pde_head = nn.Linear(d_pair, ch.num_pde_bins, bias=False) - self.resolved_ln = LayerNorm(d_single) + self.resolved_ln = nn.LayerNorm(d_single, dtype=torch.float32) # 2 = resolved logits ([unresolved, resolved]). self.resolved_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, 2)) @@ -351,7 +319,7 @@ def __init__(self, config: ESMFold2Config) -> None: else: self.lm_encoder = None - self.parcae_input_norm = LayerNorm(d_pair) + self.parcae_input_norm = nn.LayerNorm(d_pair, dtype=torch.float32) self.parcae_log_a = nn.Parameter(torch.zeros(d_pair)) parcae_decay_init = math.sqrt(1.0 / 5.0) parcae_delta_init = -math.log(parcae_decay_init) @@ -429,10 +397,10 @@ def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" import torch._dynamo - torch._dynamo.config.cache_size_limit = 512 # type: ignore[attr-defined] - torch._dynamo.config.accumulated_cache_size_limit = 512 # type: ignore[attr-defined] + torch._dynamo.config.cache_size_limit = 512 + torch._dynamo.config.accumulated_cache_size_limit = 512 # capture_scalar_outputs avoids graph breaks at .item() in atom-attention path. - torch._dynamo.config.capture_scalar_outputs = True # type: ignore[attr-defined] + torch._dynamo.config.capture_scalar_outputs = True if dynamic is None: dynamic = mode == "dynamic_seqlen" @@ -453,7 +421,7 @@ def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = def _maybe_compile(module: nn.Module) -> None: if isinstance(module, compile_targets): - module.forward = torch.compile(module.forward, **kwargs) # type: ignore[assignment] + module.forward = torch.compile(module.forward, **kwargs) self.apply(_maybe_compile) @@ -794,10 +762,10 @@ def __init__( self.outer_product_mean = OuterProductMean(d_msa, d_hidden, d_pair) if not is_final_block: self.msa_pair_weighted_averaging = MSAPairWeightedAveraging(d_msa, d_pair, n_heads_msa, msa_head_width) - self.msa_transition = PairTransition(d_msa, expansion_ratio=4) + self.msa_transition = Transition(d_msa, expansion_ratio=4) self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) - self.pair_transition = PairTransition(d_pair, expansion_ratio=4) + self.pair_transition = Transition(d_pair, expansion_ratio=4) def set_chunk_size(self, chunk_size: int | None) -> None: self.outer_product_mean.set_chunk_size(chunk_size) @@ -817,10 +785,10 @@ def forward( pair = pair + self.outer_product_mean(m, msa_attention_mask) if not self.is_final_block: m = m + self.msa_pair_weighted_averaging(m, pair, pair_attention_mask) - m = m + self.msa_transition(m) + m = self.msa_transition(m) pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) - pair = pair + self.pair_transition(pair) + pair = self.pair_transition(pair) return m, pair @@ -856,7 +824,7 @@ def __init__( def set_chunk_size(self, chunk_size: int | None) -> None: for block in self.blocks: - cast(MSAEncoderBlock, block).set_chunk_size(chunk_size) + block.set_chunk_size(chunk_size) def forward( self, diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 0a6b10b84635..b7079450ae91 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -9,13 +9,9 @@ from __future__ import annotations -import random from collections.abc import Callable -from contextlib import contextmanager from functools import partial -from typing import cast -import numpy as np import torch import torch.nn as nn import torch.nn.functional as F @@ -24,68 +20,30 @@ try: - from flash_attn import ( # type: ignore[import] + from flash_attn import ( flash_attn_func, flash_attn_varlen_func, ) - from flash_attn.bert_padding import ( # type: ignore[import] + from flash_attn.bert_padding import ( index_first_axis, pad_input, ) FLASH_ATTN_AVAILABLE = True except ImportError: - flash_attn_func = None # type: ignore[assignment] - flash_attn_varlen_func = None # type: ignore[assignment] - index_first_axis = None # type: ignore[assignment] - pad_input = None # type: ignore[assignment] + flash_attn_func = None + flash_attn_varlen_func = None + index_first_axis = None + pad_input = None FLASH_ATTN_AVAILABLE = False from ...integrations import use_kernel_forward_from_hub -from ...modeling_utils import ALL_ATTENTION_FUNCTIONS # type: ignore[import] +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS +from ...processing_utils import Unpack +from ...utils import TransformersKwargs from .configuration_esmfold2 import ESMFold2Config -class LayerNorm(nn.LayerNorm): - """LayerNorm pinned to float32. - - Normalization statistics are numerically sensitive, so this computes in - float32 and keeps its weights in float32 even when the model is loaded in a - lower precision (the explicit ``dtype`` overrides ``from_pretrained(dtype=...)``). - Inputs/outputs keep their original dtype, so it drops into a bf16 model - transparently. This is the standard Transformers idiom (matmuls run in the - model dtype; norms stay fp32) and replaces the model's former reliance on - autocast to keep norms in fp32. - """ - - def __init__(self, *args, **kwargs): - kwargs.setdefault("dtype", torch.float32) - super().__init__(*args, **kwargs) - - def forward(self, x: Tensor) -> Tensor: - return super().forward(x.float()).to(x.dtype) - - -class DropoutResidual(nn.Module): - """``residual + dropout(delta)`` with row/col-shared dropout.""" - - def __init__(self, r: float, batch_dim: int) -> None: - super().__init__() - assert batch_dim in (1, 2), f"batch_dim must be 1 or 2, got {batch_dim}" - self._batch_dim = batch_dim - self._r = r - self._impl = nn.Dropout(r) - - def forward(self, residual: Tensor, delta: Tensor) -> Tensor: - # Row/col-shared dropout via [1, ...] mask broadcast. - if self._r == 0.0 or not self.training: - return residual + delta - shape = list(delta.shape) - shape[self._batch_dim] = 1 - mask = self._impl(delta.new_ones(shape)) - return residual + delta * mask - - # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- @@ -222,7 +180,7 @@ class TransitionLayer(nn.Module): def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: super().__init__() hidden = n * d_model - self.norm = LayerNorm(d_model, eps=eps) + self.norm = nn.LayerNorm(d_model, eps=eps, dtype=torch.float32) self.a_proj = nn.Linear(d_model, hidden, bias=False) self.b_proj = nn.Linear(d_model, hidden, bias=False) self.out_proj = nn.Linear(hidden, d_model, bias=False) @@ -284,10 +242,6 @@ def forward(self, t_hat: Tensor) -> Tensor: # =========================================================================== -def _compute_swiglu_hidden_size(d_model: int, expansion_ratio: int) -> int: - return expansion_ratio * d_model - - class SwiGLU(nn.Module): """SwiGLU with packed w12 and output w3.""" @@ -315,7 +269,7 @@ class SwiGLUMLP(SwiGLU): """SwiGLU MLP with packed weights, no bias.""" def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: - hidden = _compute_swiglu_hidden_size(d_model, expansion_ratio) + hidden = expansion_ratio * d_model super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) @@ -324,8 +278,11 @@ def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) - # =========================================================================== -def _rotate_half(x: Tensor) -> Tensor: - x1, x2 = x.chunk(2, dim=-1) +# Copied from transformers.models.esm.modeling_esm.rotate_half +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] return torch.cat((-x2, x1), dim=-1) @@ -341,7 +298,7 @@ def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) return torch.cat( - [x[..., :ro_dim] * cos + _rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], + [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], dim=-1, ) @@ -418,28 +375,42 @@ def forward(self, x: Tensor) -> Tensor: # =========================================================================== +# Copied from transformers.models.llama.modeling_llama.repeat_kv +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim) + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) + + +# Copied from transformers.models.llama.modeling_llama.eager_attention_forward def eager_attention_forward( module: nn.Module, - query: Tensor, - key: Tensor, - value: Tensor, - attention_mask: Tensor | None, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: torch.Tensor | None, scaling: float, dropout: float = 0.0, - **kwargs, -) -> tuple[Tensor, Tensor]: - """Reference attention used as the eager backend / fallback for the v5 - attention interface. Inputs/outputs follow the ``ALL_ATTENTION_FUNCTIONS`` - convention: ``query``/``key``/``value`` are ``[B, H, S, Dh]`` and the - returned context is ``[B, S, H, Dh]``. ESMFold2 has no grouped-query - attention, so there is no ``repeat_kv``.""" - attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling + **kwargs: Unpack[TransformersKwargs], +): + key_states = repeat_kv(key, module.num_key_value_groups) + value_states = repeat_kv(value, module.num_key_value_groups) + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling if attention_mask is not None: attn_weights = attn_weights + attention_mask + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) - attn_output = torch.matmul(attn_weights, value) + attn_output = torch.matmul(attn_weights, value_states) attn_output = attn_output.transpose(1, 2).contiguous() + return attn_output, attn_weights @@ -499,16 +470,10 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: attention_params[3], attention_params[4], ) - q_unpad = index_first_axis( # type: ignore[misc] - q.reshape(-1, self.n_heads, self.head_dim), indices - ) - k_unpad = index_first_axis( # type: ignore[misc] - k.reshape(-1, self.n_heads, self.head_dim), indices - ) - v_unpad = index_first_axis( # type: ignore[misc] - v.reshape(-1, self.n_heads, self.head_dim), indices - ) - out_unpad = flash_attn_varlen_func( # type: ignore[misc] + q_unpad = index_first_axis(q.reshape(-1, self.n_heads, self.head_dim), indices) + k_unpad = index_first_axis(k.reshape(-1, self.n_heads, self.head_dim), indices) + v_unpad = index_first_axis(v.reshape(-1, self.n_heads, self.head_dim), indices) + out_unpad = flash_attn_varlen_func( q_unpad, k_unpad, v_unpad, @@ -519,9 +484,9 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: softmax_scale=self.scale, window_size=(self.half_window, self.half_window), ) - out = pad_input(out_unpad, indices, B, N) # type: ignore[misc] + out = pad_input(out_unpad, indices, B, N) elif use_flash: - out = flash_attn_func( # type: ignore[misc] + out = flash_attn_func( q, k, v, @@ -557,7 +522,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: ) out = out * valid.unsqueeze(-1).unsqueeze(-1) - out = out.to(input_dtype).reshape(B, N, -1) # type: ignore[union-attr] + out = out.to(input_dtype).reshape(B, N, -1) out = out * torch.sigmoid(self.gate_proj(x_input)) return self.out_proj(out) @@ -567,11 +532,11 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: # =========================================================================== -def _rms_adaln_raw(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: +def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift -def _gated_residual_raw(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: +def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: return x + gate * y @@ -587,12 +552,8 @@ def __init__( n_heads: int, half_window: int = 64, expansion_ratio: int = 2, - use_compile_fusions: bool = False, ) -> None: super().__init__() - self.attn_norm = nn.RMSNorm(d_atom, elementwise_affine=False) - self.ffn_norm = nn.RMSNorm(d_atom, elementwise_affine=False) - adaln_linear = nn.Linear(d_atom, 6 * d_atom, bias=False) nn.init.zeros_(adaln_linear.weight) self.adaln_modulation = nn.Sequential(nn.SiLU(), adaln_linear) @@ -600,22 +561,19 @@ def __init__( self.attn = SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) self.ffn = SwiGLUFFN(d_atom, expansion_ratio) - self._rms_adaln = torch.compile(_rms_adaln_raw) if use_compile_fusions else _rms_adaln_raw - self._gated_residual = torch.compile(_gated_residual_raw) if use_compile_fusions else _gated_residual_raw - def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: mod = self.adaln_modulation(c_l) if mod.dim() == 2: mod = mod.unsqueeze(1) shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) - attn_input = self._rms_adaln(x, scale_a, shift_a) + attn_input = _rms_adaln(x, scale_a, shift_a) attn_out = self.attn(attn_input, attention_params) - x = self._gated_residual(x, gate_a, attn_out) + x = _gated_residual(x, gate_a, attn_out) - ffn_input = self._rms_adaln(x, scale_f, shift_f) + ffn_input = _rms_adaln(x, scale_f, shift_f) ffn_out = self.ffn(ffn_input) - x = self._gated_residual(x, gate_f, ffn_out) + x = _gated_residual(x, gate_f, ffn_out) return x @@ -719,7 +677,7 @@ def __init__( self.structure_prediction = structure_prediction self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) - self.atom_norm = LayerNorm(d_atom) + self.atom_norm = nn.LayerNorm(d_atom, dtype=torch.float32) if structure_prediction: self.coords_linear = nn.Linear(6, d_atom, bias=False) @@ -874,7 +832,7 @@ def __init__( uid_rope_base_frequency=uid_rope_base_frequency, ) - self.norm = LayerNorm(d_atom) + self.norm = nn.LayerNorm(d_atom, dtype=torch.float32) self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) def forward( @@ -940,7 +898,7 @@ def __init__( nn.init.zeros_(self.out_gate.weight) nn.init.constant_(self.out_gate.bias, -2.0) else: - self.pre_norm = LayerNorm(d_model, eps=1e-5) + self.pre_norm = nn.LayerNorm(d_model, eps=1e-5, dtype=torch.float32) self.q_proj = nn.Linear(d_model, d_model, bias=True) self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) @@ -948,7 +906,7 @@ def __init__( self.out_proj = nn.Linear(d_model, d_model, bias=False) if d_pair > 0: - self.pair_norm = LayerNorm(d_pair, eps=1e-5) + self.pair_norm = nn.LayerNorm(d_pair, eps=1e-5, dtype=torch.float32) self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) def forward( @@ -1031,7 +989,7 @@ def __init__( nn.init.zeros_(self.output_gate.weight) nn.init.constant_(self.output_gate.bias, -2.0) else: - self.pre_norm = LayerNorm(d_model, eps=1e-5) + self.pre_norm = nn.LayerNorm(d_model, eps=1e-5, dtype=torch.float32) self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) self.lin_out = nn.Linear(hidden, d_model, bias=False) @@ -1147,16 +1105,16 @@ def __init__( self.c_s = c_s self.c_s_inputs = c_s_inputs - self.z_input_norm = LayerNorm(2 * c_z, eps=layer_norm_eps) + self.z_input_norm = nn.LayerNorm(2 * c_z, eps=layer_norm_eps, dtype=torch.float32) self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) self.z_transitions = nn.ModuleList( [TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] ) - self.s_input_norm = LayerNorm(c_s_inputs, eps=layer_norm_eps) + self.s_input_norm = nn.LayerNorm(c_s_inputs, eps=layer_norm_eps, dtype=torch.float32) self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) self.fourier = FourierEmbedding(fourier_dim) - self.noise_norm = LayerNorm(fourier_dim, eps=layer_norm_eps) + self.noise_norm = nn.LayerNorm(fourier_dim, eps=layer_norm_eps, dtype=torch.float32) self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) self.s_transitions = nn.ModuleList( [TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] @@ -1297,8 +1255,8 @@ def __init__( use_conditioning=True, ) - self.s_step_norm = LayerNorm(c_token) - self.token_norm = LayerNorm(c_token) + self.s_step_norm = nn.LayerNorm(c_token, dtype=torch.float32) + self.token_norm = nn.LayerNorm(c_token, dtype=torch.float32) def forward( self, @@ -1901,15 +1859,17 @@ class LanguageModelShim(nn.Module): Contains: - base_z_combine: nn.Parameter [num_layers+1] - - base_z_linear: Sequential(LayerNorm(d_model), Linear(d_model, d_z, bias=False)) - - base_z_mlp: Sequential(SingleToPair(d_z, d_z, d_z), LayerNorm(d_z)) + - base_z_linear: Sequential(nn.LayerNorm(d_model), Linear(d_model, d_z, bias=False)) + - base_z_mlp: Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z)) """ def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: super().__init__() - self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), LayerNorm(d_z)) - self.base_z_linear = nn.Sequential(LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) + self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z, dtype=torch.float32)) + self.base_z_linear = nn.Sequential( + nn.LayerNorm(d_model, dtype=torch.float32), nn.Linear(d_model, d_z, bias=False) + ) self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: @@ -1935,37 +1895,6 @@ def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: return lm_z -# =========================================================================== -# Reproducibility helper (mirrors evolutionaryscale.utils.reproducibility) -# =========================================================================== - - -@contextmanager -def _seed_context(seed: int | None, *, cuda: bool = True): - """Temporarily seed Python, NumPy, and PyTorch RNGs.""" - if seed is None: - yield - return - py_state = random.getstate() - np_state = np.random.get_state() - torch_state = torch.get_rng_state() - cuda_states = torch.cuda.get_rng_state_all() if cuda and torch.cuda.is_available() else None - seed = int(seed) % (2**32) - random.seed(seed) - np.random.seed(seed) - torch.manual_seed(seed) - if cuda and torch.cuda.is_available(): - torch.cuda.manual_seed_all(seed) - try: - yield - finally: - random.setstate(py_state) - np.random.set_state(np_state) - torch.set_rng_state(torch_state) - if cuda_states is not None: - torch.cuda.set_rng_state_all(cuda_states) - - # =========================================================================== # ESMFold2 — language-model backbone helpers # =========================================================================== @@ -2108,8 +2037,8 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None self.latent_channels = latent_channels self.flow = flow self._einsum_equation = self._FLOW_TO_EINSUM[flow] - self.norm_start = LayerNorm(self.input_channels, eps=_EPS) - self.norm_mix = LayerNorm(self.latent_channels, eps=_EPS) + self.norm_start = nn.LayerNorm(self.input_channels, eps=_EPS, dtype=torch.float32) + self.norm_mix = nn.LayerNorm(self.latent_channels, eps=_EPS, dtype=torch.float32) self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) @@ -2117,7 +2046,7 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None # Default chunked for memory on long sequences; tests override with # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 # parity checks. - self._chunk_size: int | None = 64 + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size @@ -2179,14 +2108,14 @@ def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: class Transition(nn.Module): - """LN + SwiGLU FFN with addmm-fused residual; optional Triton LN+w12+SwiGLU kernel.""" + """LayerNorm + SwiGLU feed-forward residual block, chunked along the token axis.""" def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: super().__init__() - self.norm = LayerNorm(d_model) + self.norm = nn.LayerNorm(d_model, dtype=torch.float32) self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. - self._chunk_size: int | None = 64 + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size @@ -2210,8 +2139,6 @@ def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) self.pair_transition = Transition(d_pair, expansion_ratio=expansion_ratio) - # Row-shared dropout-residual; r=0 for inference (HF model is inference-only). - self.row_drop = DropoutResidual(0.0, batch_dim=1) def set_chunk_size(self, chunk_size: int | None) -> None: self.tri_mul_out.set_chunk_size(chunk_size) @@ -2219,8 +2146,9 @@ def set_chunk_size(self, chunk_size: int | None) -> None: self.pair_transition.set_chunk_size(chunk_size) def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: - pair = self.row_drop(pair, self.tri_mul_out(pair, mask=pair_attention_mask)) - pair = self.row_drop(pair, self.tri_mul_in(pair, mask=pair_attention_mask)) + # HF model is inference-only, so the trained row-shared dropout (r=0) is a no-op. + pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) + pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) pair = self.pair_transition(pair) return pair @@ -2236,13 +2164,13 @@ def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = def set_chunk_size(self, chunk_size: int | None) -> None: for block in self.blocks: - cast(PairUpdateBlock, block).set_chunk_size(chunk_size) + block.set_chunk_size(chunk_size) def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: for block in self.blocks: fn = partial(block, pair_attention_mask=pair_attention_mask) if torch.is_grad_enabled(): - pair = checkpoint(fn, pair, use_reentrant=False) # pyright: ignore + pair = checkpoint(fn, pair, use_reentrant=False) else: pair = fn(pair) return pair @@ -2276,7 +2204,7 @@ def __init__( super().__init__() self.d_hidden = d_hidden self.divide_outer_before_proj = divide_outer_before_proj - self.norm = LayerNorm(d_msa) + self.norm = nn.LayerNorm(d_msa, dtype=torch.float32) self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. @@ -2317,8 +2245,10 @@ def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = super().__init__() self.n_heads = n_heads self.head_width = head_width - self.norm_single = LayerNorm(d_msa) - self.compute_bias = nn.Sequential(LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) + self.norm_single = nn.LayerNorm(d_msa, dtype=torch.float32) + self.compute_bias = nn.Sequential( + nn.LayerNorm(d_pair, dtype=torch.float32), nn.Linear(d_pair, n_heads, bias=False) + ) self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) From 60de6ca4013e49a6157376ceabf216417e127217 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Jun 2026 17:02:56 +0100 Subject: [PATCH 30/70] More cleanup --- .../models/esmfold2/configuration_esmfold2.py | 12 ---------- .../models/esmfold2/modeling_esmfold2.py | 1 - .../esmfold2/modeling_esmfold2_common.py | 24 +------------------ utils/check_config_attributes.py | 12 +++------- 4 files changed, 4 insertions(+), 45 deletions(-) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 539a5b906b71..46af732d5745 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -200,15 +200,6 @@ class ESMFold2Config(PreTrainedConfig): Number of trunk loops for iterative refinement. num_diffusion_samples (`int`, *optional*, defaults to 8): Number of parallel structure predictions to generate. - disable_msa_features (`bool`, *optional*, defaults to `False`): - When `True`, zero out MSA-derived `profile` and `deletion_mean` before - the inputs embedder. - lm_dropout (`float`, *optional*, defaults to 0.0): - Dropout probability on LM pair embeddings. When > 0, dropout is applied - with `training=True` (including at inference) to match the binder-design - training recipe. - force_lm_dropout_during_inference (`bool`, *optional*, defaults to `False`): - When `True`, apply `lm_dropout` even under `model.eval()`. lm_d_model (`int`, *optional*, defaults to 2560): Hidden size of the ESMC language-model backbone. lm_num_layers (`int`, *optional*, defaults to 80): @@ -268,9 +259,6 @@ class ESMFold2Config(PreTrainedConfig): n_relative_chain_bins: int | None = 2 num_loops: int | None = 10 num_diffusion_samples: int | None = 8 - disable_msa_features: bool | None = False - lm_dropout: float | None = 0.0 - force_lm_dropout_during_inference: bool | None = False lm_d_model: int | None = 2560 lm_num_layers: int | None = 80 esmc_id: str | None = _DEFAULT_ESMC_HF_REPO diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index b4b550abe4eb..f6e48a953aed 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -678,7 +678,6 @@ def forward( structure_output = self.structure_head.sample( z_trunk=z, s_inputs=x_inputs, - s_trunk=None, relative_position_encoding=relative_position_encoding, ref_pos=ref_pos, ref_charge=ref_charge, diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index b7079450ae91..e7c7d03b9a18 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -709,8 +709,6 @@ def forward( atom_to_token: Tensor, r_l: Tensor | None = None, pred_r1: Tensor | None = None, - s_i: Tensor | None = None, - z_ij: Tensor | None = None, num_diffusion_samples: int = 1, return_intermediates: bool = False, inference_cache: dict | None = None, @@ -914,7 +912,6 @@ def forward( a: Tensor, s: Tensor | None, z: Tensor, - beta: Tensor | float = 0.0, attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, ) -> Tensor: @@ -1059,7 +1056,6 @@ def forward( a: Tensor, s: Tensor | None, z: Tensor, - beta: Tensor | float = 0.0, attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, return_intermediates: bool = False, @@ -1071,7 +1067,6 @@ def forward( x, s, z, - beta, attention_mask=attention_mask, num_diffusion_samples=num_diffusion_samples, ) @@ -1124,7 +1119,6 @@ def forward( self, t_hat: Tensor, s_inputs: Tensor, - s_trunk: Tensor | None, z_trunk: Tensor, relative_position_encoding: Tensor, sigma_data: float | None = None, @@ -1270,7 +1264,6 @@ def forward( ref_space_uid: Tensor, tok_idx: Tensor, s_inputs: Tensor, - s_trunk: Tensor | None, z_trunk: Tensor, relative_position_encoding: Tensor, asym_id: Tensor, @@ -1281,7 +1274,6 @@ def forward( sigma_data: float | None = None, token_attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, - return_token_repr: bool = False, return_atom_repr: bool = False, inference_cache: dict[str, Tensor] | None = None, ) -> dict[str, Tensor | None]: @@ -1295,7 +1287,6 @@ def forward( s, z = self.conditioning( t_hat=t, s_inputs=s_inputs, - s_trunk=s_trunk, z_trunk=z_trunk, relative_position_encoding=relative_position_encoding, sigma_data=sigma, @@ -1317,7 +1308,6 @@ def forward( ref_atom_name_chars=ref_atom_name_chars, atom_to_token=tok_idx, r_l=r_noisy, - s_i=s_trunk, num_diffusion_samples=num_diffusion_samples, return_intermediates=return_atom_repr, inference_cache=inference_cache, @@ -1331,7 +1321,6 @@ def forward( a, s, z, - beta=0.0, attention_mask=token_attention_mask, num_diffusion_samples=num_diffusion_samples, ) @@ -1366,7 +1355,6 @@ def forward( return { "x_denoised": out, - "token_repr": a if return_token_repr else None, "atom_intermediates": atom_intermediates, } @@ -1509,7 +1497,6 @@ def sample( self, z_trunk: Tensor, s_inputs: Tensor, - s_trunk: Tensor | None, relative_position_encoding: Tensor, ref_pos: Tensor, ref_charge: Tensor, @@ -1567,7 +1554,6 @@ def sample( ) x_denoised_prev: Tensor | None = None - token_repr: Tensor | None = None diff_atom_intermediates: Tensor | None = None step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) @@ -1595,7 +1581,6 @@ def sample( ref_space_uid=ref_space_uid, tok_idx=tok_idx, s_inputs=s_inputs, - s_trunk=s_trunk, z_trunk=z_trunk, relative_position_encoding=relative_position_encoding, asym_id=asym_id, @@ -1605,13 +1590,11 @@ def sample( sym_id=sym_id, token_attention_mask=token_attention_mask, num_diffusion_samples=num_diffusion_samples, - return_token_repr=True, return_atom_repr=request_atom_repr, inference_cache=inference_cache, ) x_denoised = dm_out["x_denoised"] - token_repr = dm_out["token_repr"] if request_atom_repr: diff_atom_intermediates = dm_out.get("atom_intermediates") @@ -1644,7 +1627,6 @@ def sample( result: dict[str, Tensor | None] = { "sample_atom_coords": x, - "diff_token_repr": token_repr, } if return_atom_repr: result["diff_atom_intermediates"] = diff_atom_intermediates @@ -1872,13 +1854,11 @@ def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> ) self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) - def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: + def forward(self, hidden_states: Tensor) -> Tensor: """Project pre-computed ESMC hidden states to pair representation. Args: hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. - lm_dropout: Dropout probability applied to the pair - representation after ``base_z_mlp``. Returns: [B, L, L, d_pair] pair representation. @@ -1890,8 +1870,6 @@ def forward(self, hidden_states: Tensor, *, lm_dropout: float = 0.0) -> Tensor: weights = self.base_z_combine.softmax(0) # [81] lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] lm_z = self.base_z_mlp(lm_z) # [B, L, L, d_z] - if lm_dropout > 0: - lm_z = F.dropout(lm_z, p=lm_dropout, training=True) return lm_z diff --git a/utils/check_config_attributes.py b/utils/check_config_attributes.py index e273911eb8ca..64bc43860f47 100644 --- a/utils/check_config_attributes.py +++ b/utils/check_config_attributes.py @@ -146,15 +146,9 @@ "GlmMoeDsaConfig": ["head_dim", "layer_types", "mlp_bias", "first_k_dense_replace", "n_routed_experts"], "EsmFoldConfig": ["esm_ablate_pairwise", "esm_ablate_sequence", "esm_input_dropout", "esm_type"], "TrunkConfig": ["cpu_grad_checkpoint", "layer_drop"], - # type: architecture-variant marker (validated, "release"-only). The other - # three are training/experimental recipe knobs not consulted by the core - # single-sequence inference path (per-loop LM dropout reads lm_encoder.lm_dropout). - "ESMFold2Config": [ - "disable_msa_features", - "force_lm_dropout_during_inference", - "lm_dropout", - "type", - ], + # type: architecture-variant marker (validated, "release"-only), read in + # __post_init__ which the checker can't scan. + "ESMFold2Config": ["type"], # ESMFold2 sub-configs: their fields are threaded into submodules as explicit # dims (e.g. ESMFold2AtomEncoder(d_atom=cfg.inputs.atom_encoder.d_atom, ...)), # never read as `config.`, so the checker's heuristic can't trace them. From 2fd10604691cd092c915c5605a02680b82ba45fe Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 8 Jun 2026 17:58:52 +0100 Subject: [PATCH 31/70] dtype cleanups to make outputs match the original fork --- src/transformers/models/esmc/modeling_esmc.py | 48 +++++------ src/transformers/models/esmc/modular_esmc.py | 22 ++++- .../models/esmfold2/modeling_esmfold2.py | 17 ++-- .../esmfold2/modeling_esmfold2_common.py | 83 +++++++++++-------- 4 files changed, 98 insertions(+), 72 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index fa2cae80cfbe..45fe0fb42f71 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -28,7 +28,6 @@ from torch.nn import functional as F from ... import initialization as init -from ...integrations import use_kernel_func_from_hub from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] MaskedLMOutput, @@ -248,33 +247,6 @@ def rotate_half(x): return torch.cat((-x2, x1), dim=-1) -@use_kernel_func_from_hub("rotary_pos_emb") -def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): - """Applies Rotary Position Embedding to the query and key tensors. - - Args: - q (`torch.Tensor`): The query tensor. - k (`torch.Tensor`): The key tensor. - cos (`torch.Tensor`): The cosine part of the rotary embedding. - sin (`torch.Tensor`): The sine part of the rotary embedding. - unsqueeze_dim (`int`, *optional*, defaults to 1): - The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and - sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note - that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and - k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes - cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have - the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. - Returns: - `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. - """ - original_dtype = q.dtype - cos = cos.unsqueeze(unsqueeze_dim) - sin = sin.unsqueeze(unsqueeze_dim) - q_embed = (q.float() * cos) + (rotate_half(q.float()) * sin) - k_embed = (k.float() * cos) + (rotate_half(k.float()) * sin) - return q_embed.to(original_dtype), k_embed.to(original_dtype) - - def eager_attention_forward( module: nn.Module, query: torch.Tensor, @@ -303,6 +275,26 @@ def eager_attention_forward( return attn_output, attn_weights +def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): + """Apply Rotary Position Embedding to ``q`` and ``k`` in the activation dtype. + + This deliberately differs from the otherwise-identical + :func:`~transformers.models.esm.modeling_esm.apply_rotary_pos_emb` (whose + ``rotate_half`` it reuses): that helper upcasts ``q``/``k`` to fp32 for the + rotation, but the reference ESMC implementation applies RoPE in the + activation dtype. Upcasting here would make bf16 inference diverge from the + published ESMC numerics — at bf16 it is the single source of fork-vs-port + drift, accumulating over the residual stream (~0.3 over 80 layers on + ESMC-6B). The rotation is a no-op-difference in fp32 (``q`` is already fp32), + so fp32 stays bit-exact. See ``modeling_esm`` for the argument semantics. + """ + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_embed = (q * cos) + (rotate_half(q) * sin) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: """LayerNorm + fused QKV projection.""" assert not bias, "ESMC was trained with bias=False; bias=True not supported" diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 3164b6f73ee3..31d3c338c521 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -37,8 +37,8 @@ logging, ) from ..esm.modeling_esm import ( - apply_rotary_pos_emb, eager_attention_forward, + rotate_half, ) from .configuration_esmc import ESMCConfig @@ -191,6 +191,26 @@ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Te return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) +def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): + """Apply Rotary Position Embedding to ``q`` and ``k`` in the activation dtype. + + This deliberately differs from the otherwise-identical + :func:`~transformers.models.esm.modeling_esm.apply_rotary_pos_emb` (whose + ``rotate_half`` it reuses): that helper upcasts ``q``/``k`` to fp32 for the + rotation, but the reference ESMC implementation applies RoPE in the + activation dtype. Upcasting here would make bf16 inference diverge from the + published ESMC numerics — at bf16 it is the single source of fork-vs-port + drift, accumulating over the residual stream (~0.3 over 80 layers on + ESMC-6B). The rotation is a no-op-difference in fp32 (``q`` is already fp32), + so fp32 stays bit-exact. See ``modeling_esm`` for the argument semantics. + """ + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_embed = (q * cos) + (rotate_half(q) * sin) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + # --------------------------------------------------------------------------- # Feed-forward network helpers # --------------------------------------------------------------------------- diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index f6e48a953aed..21ae41ce7cef 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -123,9 +123,9 @@ def forward( relative_position_encoding: Tensor | None = None, token_bonds_encoding: Tensor | None = None, ) -> dict[str, Tensor]: - s_inputs_normed = self.s_inputs_norm(s_inputs) + s_inputs_normed = self.s_inputs_norm(s_inputs.float()).to(s_inputs.dtype) - z_base = self.z_norm(z) + z_base = self.z_norm(z.float()).to(z.dtype) if relative_position_encoding is not None: z_base = z_base + relative_position_encoding if token_bonds_encoding is not None: @@ -162,7 +162,7 @@ def forward( atom_mask_f = atom_mask_m.float() s_at_atoms = gather_token_to_atom(single, atom_to_token_m) - s_at_atoms_ln = self.plddt_ln(s_at_atoms) + s_at_atoms_ln = self.plddt_ln(s_at_atoms.float()).to(s_at_atoms.dtype) intra_idx = _compute_intra_token_idx(atom_to_token_m) intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) @@ -198,15 +198,15 @@ def forward( plddt_ca = plddt_per_atom.gather(1, rep_idx_m) # PAE - pae_logits = self.pae_head(self.pae_ln(pair)) + pae_logits = self.pae_head(self.pae_ln(pair.float()).to(pair.dtype)) pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() # PDE - pde_logits = self.pde_head(self.pde_ln(pair)) + pde_logits = self.pde_head(self.pde_ln(pair.float()).to(pair.dtype)) pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() # Resolved (per-atom binary). - s_at_atoms_res = self.resolved_ln(s_at_atoms) + s_at_atoms_res = self.resolved_ln(s_at_atoms.float()).to(s_at_atoms.dtype) w_res = self.resolved_weight[intra_idx] resolved_logits = torch.einsum("...c,...cb->...b", s_at_atoms_res, w_res) @@ -289,6 +289,9 @@ class ESMFold2Model(PreTrainedModel): config_class = ESMFold2Config _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + # The Fourier noise-embedding frequencies/phases are random Gaussian features whose + # precision drives the diffusion conditioning; keep them fp32 even under dtype=bf16. + _keep_in_fp32_modules_strict = ["fourier"] _supports_sdpa = True _supports_flash_attn = True _supports_attention_backend = True @@ -510,7 +513,7 @@ def _run_one_loop( if refined_lm_z is not None: z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) - injected_pair = self.parcae_input_norm(z_inject_pair) + injected_pair = self.parcae_input_norm(z_inject_pair.float()).to(z_inject_pair.dtype) z = a * z + F.linear(injected_pair.to(z.dtype), b_mat) z = self.folding_trunk(z, pair_attention_mask=pair_mask) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index e7c7d03b9a18..318c14abbfe7 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -186,10 +186,10 @@ def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: self.out_proj = nn.Linear(hidden, d_model, bias=False) def forward(self, x: Tensor) -> Tensor: - x = self.norm(x) + x = self.norm(x.float()).to(x.dtype) a = self.a_proj(x) b = self.b_proj(x) - return self.out_proj(F.silu(a) * b) + return self.out_proj((F.silu(a.float()) * b.float()).to(a.dtype)) # =========================================================================== @@ -210,9 +210,12 @@ def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: self.s_shift = nn.Linear(d_cond, d_model, bias=False) def forward(self, a: Tensor, s: Tensor) -> Tensor: - a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps).to(a.dtype) + a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) - return torch.sigmoid(self.s_gate(s_norm)) * a_norm + self.s_shift(s_norm) + # gate/shift come from bf16 linears; do the gating + affine in fp32, downcast at the end. + gate = torch.sigmoid(self.s_gate(s_norm).float()) + shift = self.s_shift(s_norm).float() + return (gate * a_norm + shift).to(a.dtype) # =========================================================================== @@ -233,6 +236,8 @@ def __init__(self, c: int) -> None: self.register_buffer("b", torch.randn(c)) def forward(self, t_hat: Tensor) -> Tensor: + # w/b are kept fp32 (ESMFold2Model._keep_in_fp32_modules_strict), so the random + # frequencies/phases — and the cos embedding — are computed at full precision. t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) @@ -261,7 +266,7 @@ def __init__( def forward(self, x: Tensor) -> Tensor: x12 = self.w12(x) x1, x2 = x12.split(self.hidden_features, dim=-1) - hidden = F.silu(x1) * x2 + hidden = (F.silu(x1.float()) * x2.float()).to(x1.dtype) return self.w3(hidden) @@ -347,7 +352,7 @@ def build_3d_rope( def qk_norm(x: Tensor) -> Tensor: - return F.rms_norm(x, (x.size(-1),)).to(x.dtype) + return F.rms_norm(x.float(), (x.size(-1),)).to(x.dtype) # =========================================================================== @@ -367,7 +372,7 @@ def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: def forward(self, x: Tensor) -> Tensor: x = x.to(self.w_up.weight.dtype) x1, x2 = self.w_up(x).chunk(2, dim=-1) - return self.w_down(F.silu(x1) * x2) + return self.w_down((F.silu(x1.float()) * x2.float()).to(x1.dtype)) # =========================================================================== @@ -523,7 +528,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: out = out * valid.unsqueeze(-1).unsqueeze(-1) out = out.to(input_dtype).reshape(B, N, -1) - out = out * torch.sigmoid(self.gate_proj(x_input)) + out = (out.float() * torch.sigmoid(self.gate_proj(x_input).float())).to(input_dtype) return self.out_proj(out) @@ -533,11 +538,11 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: - return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift + return (F.rms_norm(x.float(), (x.shape[-1],)) * (1 + scale.float()) + shift.float()).to(x.dtype) def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: - return x + gate * y + return (x.float() + gate.float() * y.float()).to(x.dtype) class SWAAtomBlock(nn.Module): @@ -735,7 +740,9 @@ def forward( ], dim=-1, ) - c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype))) + c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( + self.atom_linear.weight.dtype + ) cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) cos = cos.repeat_interleave(num_diffusion_samples, 0) sin = sin.repeat_interleave(num_diffusion_samples, 0) @@ -862,7 +869,7 @@ def forward( q_l = result intermediates = [] - r_l = self.output_linear(self.norm(q_l)) + r_l = self.output_linear(self.norm(q_l.float()).to(q_l.dtype)) return r_l, intermediates @@ -920,7 +927,7 @@ def forward( if s is not None: x = self.adaln(a, s) else: - x = self.pre_norm(a) + x = self.pre_norm(a.float()).to(a.dtype) n_keys = x.shape[1] q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) @@ -936,12 +943,12 @@ def forward( attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) # Standard attention with pair bias - g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) + g = torch.sigmoid(self.g_proj(x).float()).view(bsz, n_queries, self.num_heads, self.head_dim) logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale if z.dim() == 4: - pair_bias = self.pair_bias_proj(self.pair_norm(z)) + pair_bias = self.pair_bias_proj(self.pair_norm(z.float()).to(z.dtype)) else: pair_bias = z.unsqueeze(-1) logits = logits + pair_bias.to(dtype=logits.dtype) @@ -953,11 +960,11 @@ def forward( attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) - ctx = g * ctx - out = self.out_proj(ctx.reshape(bsz, n_queries, d_model)) + ctx = g * ctx.float() + out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) if s is not None: - out = torch.sigmoid(self.out_gate(s)) * out + out = (torch.sigmoid(self.out_gate(s).float()) * out.float()).to(out.dtype) return out @@ -995,14 +1002,14 @@ def forward(self, a: Tensor, s: Tensor | None) -> Tensor: if s is not None: x = self.adaln(a, s) else: - x = self.pre_norm(a) + x = self.pre_norm(a.float()).to(a.dtype) swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) - b = F.silu(swish_a) * swish_b + b = (F.silu(swish_a.float()) * swish_b.float()).to(swish_a.dtype) out = self.lin_out(b) if s is not None: - out = torch.sigmoid(self.output_gate(s)) * out + out = (torch.sigmoid(self.output_gate(s).float()) * out.float()).to(out.dtype) return out @@ -1158,7 +1165,7 @@ def forward( t = t.repeat_interleave(num_diffusion_samples, 0) t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) n = self.fourier(t_noise) - n = self.noise_proj(self.noise_norm(n)) + n = self.noise_proj(self.noise_norm(n.float()).to(self.noise_proj.weight.dtype)) s = s + n.unsqueeze(1) for block in self.s_transitions: @@ -1314,7 +1321,7 @@ def forward( ) # Step 4: add conditioned s - a = a + self.s_to_token(self.s_step_norm(s)) + a = a + self.s_to_token(self.s_step_norm(s.float()).to(s.dtype)) # Step 5: token transformer a, _ = self.token_transformer( @@ -1326,7 +1333,7 @@ def forward( ) # Step 6: token norm - a = self.token_norm(a) + a = self.token_norm(a.float()).to(a.dtype) # Step 7: atom decoder r_update, dec_intermediates = self.atom_decoder( @@ -1866,10 +1873,14 @@ def forward(self, hidden_states: Tensor) -> Tensor: # The ESMC backbone may be loaded at a different precision than the trunk # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) - lm_z = self.base_z_linear(hidden_states) # [B, L, 81, d_z] + # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. + normed = self.base_z_linear[0](hidden_states.float()).to(hidden_states.dtype) + lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] weights = self.base_z_combine.softmax(0) # [81] lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] - lm_z = self.base_z_mlp(lm_z) # [B, L, L, d_z] + # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. + pair = self.base_z_mlp[0](lm_z) + lm_z = self.base_z_mlp[1](pair.float()).to(pair.dtype) # [B, L, L, d_z] return lm_z @@ -2049,10 +2060,10 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor if visibility is None: visibility = pair_grid.new_ones(pair_grid.shape[:-1]) - normalized_grid = self.norm_start(pair_grid) + normalized_grid = self.norm_start(pair_grid.float()).to(pair_grid.dtype) bundled = self.proj_bundle(normalized_grid) signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) - routed = signal * torch.sigmoid(gate_logits) + routed = signal.float() * torch.sigmoid(gate_logits.float()) routed = routed * visibility.unsqueeze(-1) left_stream, right_stream = routed.float().chunk(2, dim=-1) @@ -2061,8 +2072,8 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor else: contracted = self._triangular_contract(left_stream, right_stream) mixed = self.proj_emit(self.norm_mix(contracted).to(self.proj_emit.weight.dtype)) - output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) - return mixed * output_gate + output_gate = torch.sigmoid(self.proj_gate(normalized_grid).float()) + return (mixed.float() * output_gate).to(mixed.dtype) class TriangleMultiplicativeUpdate(nn.Module): @@ -2100,12 +2111,12 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def forward(self, x: Tensor) -> Tensor: if self._chunk_size is None or x.shape[1] <= self._chunk_size: - return x + self.ffn(self.norm(x)) + return x + self.ffn(self.norm(x.float()).to(x.dtype)) out_list: list[Tensor] = [] for s in range(0, x.shape[1], self._chunk_size): e = min(s + self._chunk_size, x.shape[1]) sl = x[:, s:e] - out_list.append(sl + self.ffn(self.norm(sl))) + out_list.append(sl + self.ffn(self.norm(sl.float()).to(sl.dtype))) return torch.cat(out_list, dim=1) @@ -2192,7 +2203,7 @@ def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: - m_norm = self.norm(m) + m_norm = self.norm(m.float()).to(m.dtype) x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) a, b = x.chunk(2, dim=-1) mask_f = msa_attention_mask.to(a.dtype) @@ -2243,13 +2254,13 @@ def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tens B, L, M, _ = msa_repr.shape h, dh = self.n_heads, self.head_width - msa_normed = self.norm_single(msa_repr) - bias = self.compute_bias(pair_repr) # [B, L, L, n_heads] + msa_normed = self.norm_single(msa_repr.float()).to(msa_repr.dtype) + bias = self.compute_bias[1](self.compute_bias[0](pair_repr.float()).to(pair_repr.dtype)) # [B, L, L, n_heads] bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j v = self.Wv(msa_normed).reshape(B, L, M, h, dh) - gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) + gate = torch.sigmoid(self.Wgate(msa_normed).float()).to(msa_normed.dtype).reshape(B, L, M, h, dh) output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) return self.Wout(output.reshape(B, L, M, h * dh)) From 784ece79ee532d7eb3fe4c487dc9d857c45025a8 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jun 2026 12:37:15 +0100 Subject: [PATCH 32/70] More chasing dtypes --- src/transformers/models/esmc/modeling_esmc.py | 61 ++++++++++++++++--- src/transformers/models/esmc/modular_esmc.py | 61 ++++++++++++++++--- .../models/esmfold2/modeling_esmfold2.py | 18 +++++- 3 files changed, 121 insertions(+), 19 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 45fe0fb42f71..2552de1ed660 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -172,6 +172,17 @@ def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float32) / dim) ) + def _apply(self, fn, recurse=True): + # Keep ``inv_freq`` in fp32 even when the module is cast to bf16/fp16 (e.g. + # ``ESMFold2Model.load_esmc`` does ``.to(bf16)``). ``super()._apply`` would + # round the rotary frequencies to bf16, which drifts the RoPE angles and is + # the dominant source of bf16 fork-vs-port divergence in the ESMFold2 backbone. + result = super()._apply(fn, recurse=recurse) + self.register_buffer( + "inv_freq", self._compute_inv_freq(self.config, device=self.inv_freq.device), persistent=False + ) + return result + @torch.no_grad() def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) @@ -185,8 +196,27 @@ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Te return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) +class _Fp32LayerNorm(nn.LayerNorm): + """LayerNorm whose normalisation is always computed in fp32, then cast back + to the input dtype. + + This matches the reference ESMC numerics: the original model runs its + LayerNorms in fp32 (under a bf16 autocast), never in bf16. We reproduce that + explicitly so the result is independent of any surrounding autocast and is a + no-op in fp32 (every cast below is identity when ``x`` is already fp32). The + norm *weights* are kept fp32 on a bf16 standalone load via + ``_keep_in_fp32_modules_strict``; inside ESMFold2 they are bf16-rounded to + match the trained checkpoint and upcast here for the fp32 maths. + """ + + def forward(self, x: torch.Tensor) -> torch.Tensor: + weight = self.weight.float() if self.weight is not None else None + bias = self.bias.float() if self.bias is not None else None + return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) + + class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection. + """LayerNorm (computed in fp32) followed by a Linear projection. Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and ``weight`` to match the published ESMC checkpoint layout. @@ -202,7 +232,10 @@ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: nn.init.normal_(self.weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) + # Norm in fp32 (matches the reference), then the projection in x's dtype. + x = F.layer_norm( + x.float(), (self.d_in,), self.layer_norm_weight.float(), self.layer_norm_bias.float(), self.eps + ).to(x.dtype) return F.linear(x, self.weight) @@ -227,13 +260,14 @@ def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> nn.init.normal_(self.fc2_weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: + # Norm in fp32 (matches the reference), then the MLP in x's dtype. x = F.layer_norm( - x, + x.float(), (self.hidden_size,), - self.layer_norm_weight, - self.layer_norm_bias, + self.layer_norm_weight.float(), + self.layer_norm_bias.float(), self.eps, - ) + ).to(x.dtype) x = F.linear(x, self.fc1_weight) x1, x2 = x.chunk(2, dim=-1) x = F.silu(x1) * x2 @@ -347,8 +381,8 @@ def __init__( self.out_proj = _make_attn_out_proj(d_model, bias) if qk_layernorm: - self.q_ln = nn.LayerNorm(d_model, bias=bias) - self.k_ln = nn.LayerNorm(d_model, bias=bias) + self.q_ln = _Fp32LayerNorm(d_model, bias=bias) + self.k_ln = _Fp32LayerNorm(d_model, bias=bias) else: self.q_ln = nn.Identity() self.k_ln = nn.Identity() @@ -555,7 +589,7 @@ def __init__( for _ in range(n_layers) ] ) - self.norm = nn.LayerNorm(d_model, bias=False) + self.norm = _Fp32LayerNorm(d_model, bias=False) def forward( self, @@ -635,6 +669,15 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_attention_backend = True _no_split_modules = ["UnifiedTransformerBlock"] _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + # ESMC computes its LayerNorms in fp32 (see ``_Fp32LayerNorm``). When the + # model is loaded in bf16 standalone we also keep the norm *weights* in fp32 + # (the standard Transformers idiom). These patterns match only the norm + # parameters — the fused ``layer_norm_weight``/``layer_norm_bias`` plus the + # ``q_ln``/``k_ln`` and final ``transformer.norm`` — never the projection + # weights in the same fused modules. (Inside ESMFold2 the backbone is loaded + # bf16 and the norms bf16-rounded to match the trained checkpoint; see + # ``ESMFold2Model.load_esmc``.) + _keep_in_fp32_modules_strict = ["layer_norm_weight", "layer_norm_bias", "q_ln", "k_ln", "transformer.norm"] def _init_weights(self, module: nn.Module): std = self.config.initializer_range diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 31d3c338c521..2fbcb984cc69 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -178,6 +178,17 @@ def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float32) / dim) ) + def _apply(self, fn, recurse=True): + # Keep ``inv_freq`` in fp32 even when the module is cast to bf16/fp16 (e.g. + # ``ESMFold2Model.load_esmc`` does ``.to(bf16)``). ``super()._apply`` would + # round the rotary frequencies to bf16, which drifts the RoPE angles and is + # the dominant source of bf16 fork-vs-port divergence in the ESMFold2 backbone. + result = super()._apply(fn, recurse=recurse) + self.register_buffer( + "inv_freq", self._compute_inv_freq(self.config, device=self.inv_freq.device), persistent=False + ) + return result + @torch.no_grad() def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) @@ -221,8 +232,27 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: return int(((expansion_ratio * d_model) + 255) // 256 * 256) +class _Fp32LayerNorm(nn.LayerNorm): + """LayerNorm whose normalisation is always computed in fp32, then cast back + to the input dtype. + + This matches the reference ESMC numerics: the original model runs its + LayerNorms in fp32 (under a bf16 autocast), never in bf16. We reproduce that + explicitly so the result is independent of any surrounding autocast and is a + no-op in fp32 (every cast below is identity when ``x`` is already fp32). The + norm *weights* are kept fp32 on a bf16 standalone load via + ``_keep_in_fp32_modules_strict``; inside ESMFold2 they are bf16-rounded to + match the trained checkpoint and upcast here for the fp32 maths. + """ + + def forward(self, x: torch.Tensor) -> torch.Tensor: + weight = self.weight.float() if self.weight is not None else None + bias = self.bias.float() if self.bias is not None else None + return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) + + class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection. + """LayerNorm (computed in fp32) followed by a Linear projection. Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and ``weight`` to match the published ESMC checkpoint layout. @@ -238,7 +268,10 @@ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: nn.init.normal_(self.weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) + # Norm in fp32 (matches the reference), then the projection in x's dtype. + x = F.layer_norm( + x.float(), (self.d_in,), self.layer_norm_weight.float(), self.layer_norm_bias.float(), self.eps + ).to(x.dtype) return F.linear(x, self.weight) @@ -263,13 +296,14 @@ def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> nn.init.normal_(self.fc2_weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: + # Norm in fp32 (matches the reference), then the MLP in x's dtype. x = F.layer_norm( - x, + x.float(), (self.hidden_size,), - self.layer_norm_weight, - self.layer_norm_bias, + self.layer_norm_weight.float(), + self.layer_norm_bias.float(), self.eps, - ) + ).to(x.dtype) x = F.linear(x, self.fc1_weight) x1, x2 = x.chunk(2, dim=-1) x = F.silu(x1) * x2 @@ -345,8 +379,8 @@ def __init__( self.out_proj = _make_attn_out_proj(d_model, bias) if qk_layernorm: - self.q_ln = nn.LayerNorm(d_model, bias=bias) - self.k_ln = nn.LayerNorm(d_model, bias=bias) + self.q_ln = _Fp32LayerNorm(d_model, bias=bias) + self.k_ln = _Fp32LayerNorm(d_model, bias=bias) else: self.q_ln = nn.Identity() self.k_ln = nn.Identity() @@ -526,7 +560,7 @@ def __init__( for _ in range(n_layers) ] ) - self.norm = nn.LayerNorm(d_model, bias=False) + self.norm = _Fp32LayerNorm(d_model, bias=False) def forward( self, @@ -606,6 +640,15 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_attention_backend = True _no_split_modules = ["UnifiedTransformerBlock"] _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + # ESMC computes its LayerNorms in fp32 (see ``_Fp32LayerNorm``). When the + # model is loaded in bf16 standalone we also keep the norm *weights* in fp32 + # (the standard Transformers idiom). These patterns match only the norm + # parameters — the fused ``layer_norm_weight``/``layer_norm_bias`` plus the + # ``q_ln``/``k_ln`` and final ``transformer.norm`` — never the projection + # weights in the same fused modules. (Inside ESMFold2 the backbone is loaded + # bf16 and the norms bf16-rounded to match the trained checkpoint; see + # ``ESMFold2Model.load_esmc``.) + _keep_in_fp32_modules_strict = ["layer_norm_weight", "layer_norm_bias", "q_ln", "k_ln", "transformer.norm"] def _init_weights(self, module: nn.Module): std = self.config.initializer_range diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 21ae41ce7cef..ed986d044398 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -378,7 +378,23 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: raise ValueError(f"precision must be one of {list(dtype_map)}, got {precision!r}") dtype = dtype_map[precision] - esmc = AutoModel.from_pretrained(esmc_model_path).to(device=self.device, dtype=dtype).eval() + # Load directly in the target dtype so the ~25GB fp32 6B backbone is never + # materialised (it would not fit alongside the trunk on a 24GB GPU). + esmc = AutoModel.from_pretrained(esmc_model_path, dtype=dtype).to(device=self.device, dtype=dtype) + if dtype == torch.bfloat16: + # The ESMFold2 checkpoint was trained against a bf16-cast backbone, so + # reproduce that exactly. ESMC keeps its norm weights in fp32 on a bf16 + # load (``_keep_in_fp32_modules_strict``); the ``.to(bf16)`` above forced + # those down to bf16, matching the trained rounding. Now upcast just the + # norm weights back to fp32 so the LayerNorms still run in fp32 (the + # reference ran them under autocast). Values stay bf16-rounded — only the + # storage/maths are fp32. Net: bit-identical to "load fp32 -> .to(bf16) -> + # autocast" without ever holding the fp32 backbone. + norm_markers = getattr(esmc, "_keep_in_fp32_modules_strict", None) or () + for name, param in esmc.named_parameters(): + if any(marker in name for marker in norm_markers): + param.data = param.data.float() + esmc = esmc.eval() for p in esmc.parameters(): p.requires_grad_(False) From fbe1db092ef9c52162f99cf0b9ac168a7566ee58 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jun 2026 17:06:40 +0100 Subject: [PATCH 33/70] More bf16 to match the original + bump speed --- .../models/esmfold2/modeling_esmfold2_common.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index 318c14abbfe7..e2ca8e401de8 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -2063,17 +2063,23 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor normalized_grid = self.norm_start(pair_grid.float()).to(pair_grid.dtype) bundled = self.proj_bundle(normalized_grid) signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) - routed = signal.float() * torch.sigmoid(gate_logits.float()) + # Gates and the O(N^3) contraction run in the activation dtype (bf16). This + # matches the reference: under its autocast the einsum is downcast to bf16, + # and the fused Triton kernel likewise contracts in bf16 — the dtype the + # checkpoint was trained with. Keeping these in fp32 was a (marginal) + # precision *up* that diverges from training and is slower on the trunk's + # dominant op. ``norm_start``/``norm_mix`` stay fp32. A no-op in fp32. + routed = signal * torch.sigmoid(gate_logits) routed = routed * visibility.unsqueeze(-1) - left_stream, right_stream = routed.float().chunk(2, dim=-1) + left_stream, right_stream = routed.chunk(2, dim=-1) if self._chunk_size is not None: contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) else: contracted = self._triangular_contract(left_stream, right_stream) - mixed = self.proj_emit(self.norm_mix(contracted).to(self.proj_emit.weight.dtype)) - output_gate = torch.sigmoid(self.proj_gate(normalized_grid).float()) - return (mixed.float() * output_gate).to(mixed.dtype) + mixed = self.proj_emit(self.norm_mix(contracted.float()).to(self.proj_emit.weight.dtype)) + output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) + return mixed * output_gate class TriangleMultiplicativeUpdate(nn.Module): From 0bba9d788a742fdc1ff3008e13b150d9734a2ef0 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jun 2026 17:19:58 +0100 Subject: [PATCH 34/70] Remove more float upcasts for speeds + closer fork match --- .../esmfold2/modeling_esmfold2_common.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py index e2ca8e401de8..e00f87ff8081 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2_common.py @@ -189,7 +189,7 @@ def forward(self, x: Tensor) -> Tensor: x = self.norm(x.float()).to(x.dtype) a = self.a_proj(x) b = self.b_proj(x) - return self.out_proj((F.silu(a.float()) * b.float()).to(a.dtype)) + return self.out_proj(F.silu(a) * b) # =========================================================================== @@ -212,9 +212,10 @@ def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: def forward(self, a: Tensor, s: Tensor) -> Tensor: a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) - # gate/shift come from bf16 linears; do the gating + affine in fp32, downcast at the end. - gate = torch.sigmoid(self.s_gate(s_norm).float()) - shift = self.s_shift(s_norm).float() + # gate/shift in bf16 (matches the reference's autocast); a_norm is fp32 so the + # affine promotes to fp32, then downcast for the next op. + gate = torch.sigmoid(self.s_gate(s_norm)) + shift = self.s_shift(s_norm) return (gate * a_norm + shift).to(a.dtype) @@ -266,7 +267,7 @@ def __init__( def forward(self, x: Tensor) -> Tensor: x12 = self.w12(x) x1, x2 = x12.split(self.hidden_features, dim=-1) - hidden = (F.silu(x1.float()) * x2.float()).to(x1.dtype) + hidden = F.silu(x1) * x2 return self.w3(hidden) @@ -352,7 +353,7 @@ def build_3d_rope( def qk_norm(x: Tensor) -> Tensor: - return F.rms_norm(x.float(), (x.size(-1),)).to(x.dtype) + return F.rms_norm(x, (x.size(-1),)) # =========================================================================== @@ -372,7 +373,7 @@ def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: def forward(self, x: Tensor) -> Tensor: x = x.to(self.w_up.weight.dtype) x1, x2 = self.w_up(x).chunk(2, dim=-1) - return self.w_down((F.silu(x1.float()) * x2.float()).to(x1.dtype)) + return self.w_down(F.silu(x1) * x2) # =========================================================================== @@ -528,7 +529,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: out = out * valid.unsqueeze(-1).unsqueeze(-1) out = out.to(input_dtype).reshape(B, N, -1) - out = (out.float() * torch.sigmoid(self.gate_proj(x_input).float())).to(input_dtype) + out = out * torch.sigmoid(self.gate_proj(x_input)) return self.out_proj(out) @@ -538,11 +539,11 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: - return (F.rms_norm(x.float(), (x.shape[-1],)) * (1 + scale.float()) + shift.float()).to(x.dtype) + return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: - return (x.float() + gate.float() * y.float()).to(x.dtype) + return x + gate * y class SWAAtomBlock(nn.Module): @@ -943,7 +944,7 @@ def forward( attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) # Standard attention with pair bias - g = torch.sigmoid(self.g_proj(x).float()).view(bsz, n_queries, self.num_heads, self.head_dim) + g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale @@ -960,11 +961,11 @@ def forward( attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) - ctx = g * ctx.float() + ctx = g * ctx out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) if s is not None: - out = (torch.sigmoid(self.out_gate(s).float()) * out.float()).to(out.dtype) + out = torch.sigmoid(self.out_gate(s)) * out return out @@ -1005,11 +1006,11 @@ def forward(self, a: Tensor, s: Tensor | None) -> Tensor: x = self.pre_norm(a.float()).to(a.dtype) swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) - b = (F.silu(swish_a.float()) * swish_b.float()).to(swish_a.dtype) + b = F.silu(swish_a) * swish_b out = self.lin_out(b) if s is not None: - out = (torch.sigmoid(self.output_gate(s).float()) * out.float()).to(out.dtype) + out = torch.sigmoid(self.output_gate(s)) * out return out @@ -2266,7 +2267,7 @@ def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tens attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j v = self.Wv(msa_normed).reshape(B, L, M, h, dh) - gate = torch.sigmoid(self.Wgate(msa_normed).float()).to(msa_normed.dtype).reshape(B, L, M, h, dh) + gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) return self.Wout(output.reshape(B, L, M, h * dh)) From 17d93c016366f0a05adc848680898c5b7fa962c5 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jun 2026 17:45:56 +0100 Subject: [PATCH 35/70] Remove more float upcasts for speeds + closer fork match --- src/transformers/models/esmc/modeling_esmc.py | 56 +++++-------------- src/transformers/models/esmc/modular_esmc.py | 56 +++++-------------- .../models/esmfold2/modeling_esmfold2.py | 47 ++++++++-------- 3 files changed, 50 insertions(+), 109 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 2552de1ed660..211aca27f90b 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -196,30 +196,15 @@ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Te return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) -class _Fp32LayerNorm(nn.LayerNorm): - """LayerNorm whose normalisation is always computed in fp32, then cast back - to the input dtype. - - This matches the reference ESMC numerics: the original model runs its - LayerNorms in fp32 (under a bf16 autocast), never in bf16. We reproduce that - explicitly so the result is independent of any surrounding autocast and is a - no-op in fp32 (every cast below is identity when ``x`` is already fp32). The - norm *weights* are kept fp32 on a bf16 standalone load via - ``_keep_in_fp32_modules_strict``; inside ESMFold2 they are bf16-rounded to - match the trained checkpoint and upcast here for the fp32 maths. - """ - - def forward(self, x: torch.Tensor) -> torch.Tensor: - weight = self.weight.float() if self.weight is not None else None - bias = self.bias.float() if self.bias is not None else None - return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) - - class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm (computed in fp32) followed by a Linear projection. + """LayerNorm followed by a Linear projection. Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and - ``weight`` to match the published ESMC checkpoint layout. + ``weight`` to match the published ESMC checkpoint layout. The LayerNorm runs + in the activation dtype (plain ``F.layer_norm``) exactly like the reference: + standalone bf16 → bf16 norm; inside ESMFold2 the backbone is wrapped in a + bf16 autocast (matching the fork's ``_lm_precision_context``), so the norm + then computes in fp32. A no-op in fp32. """ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: @@ -232,10 +217,7 @@ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: nn.init.normal_(self.weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - # Norm in fp32 (matches the reference), then the projection in x's dtype. - x = F.layer_norm( - x.float(), (self.d_in,), self.layer_norm_weight.float(), self.layer_norm_bias.float(), self.eps - ).to(x.dtype) + x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) return F.linear(x, self.weight) @@ -260,14 +242,13 @@ def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> nn.init.normal_(self.fc2_weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - # Norm in fp32 (matches the reference), then the MLP in x's dtype. x = F.layer_norm( - x.float(), + x, (self.hidden_size,), - self.layer_norm_weight.float(), - self.layer_norm_bias.float(), + self.layer_norm_weight, + self.layer_norm_bias, self.eps, - ).to(x.dtype) + ) x = F.linear(x, self.fc1_weight) x1, x2 = x.chunk(2, dim=-1) x = F.silu(x1) * x2 @@ -381,8 +362,8 @@ def __init__( self.out_proj = _make_attn_out_proj(d_model, bias) if qk_layernorm: - self.q_ln = _Fp32LayerNorm(d_model, bias=bias) - self.k_ln = _Fp32LayerNorm(d_model, bias=bias) + self.q_ln = nn.LayerNorm(d_model, bias=bias) + self.k_ln = nn.LayerNorm(d_model, bias=bias) else: self.q_ln = nn.Identity() self.k_ln = nn.Identity() @@ -589,7 +570,7 @@ def __init__( for _ in range(n_layers) ] ) - self.norm = _Fp32LayerNorm(d_model, bias=False) + self.norm = nn.LayerNorm(d_model, bias=False) def forward( self, @@ -669,15 +650,6 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_attention_backend = True _no_split_modules = ["UnifiedTransformerBlock"] _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] - # ESMC computes its LayerNorms in fp32 (see ``_Fp32LayerNorm``). When the - # model is loaded in bf16 standalone we also keep the norm *weights* in fp32 - # (the standard Transformers idiom). These patterns match only the norm - # parameters — the fused ``layer_norm_weight``/``layer_norm_bias`` plus the - # ``q_ln``/``k_ln`` and final ``transformer.norm`` — never the projection - # weights in the same fused modules. (Inside ESMFold2 the backbone is loaded - # bf16 and the norms bf16-rounded to match the trained checkpoint; see - # ``ESMFold2Model.load_esmc``.) - _keep_in_fp32_modules_strict = ["layer_norm_weight", "layer_norm_bias", "q_ln", "k_ln", "transformer.norm"] def _init_weights(self, module: nn.Module): std = self.config.initializer_range diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 2fbcb984cc69..381392694b26 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -232,30 +232,15 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: return int(((expansion_ratio * d_model) + 255) // 256 * 256) -class _Fp32LayerNorm(nn.LayerNorm): - """LayerNorm whose normalisation is always computed in fp32, then cast back - to the input dtype. - - This matches the reference ESMC numerics: the original model runs its - LayerNorms in fp32 (under a bf16 autocast), never in bf16. We reproduce that - explicitly so the result is independent of any surrounding autocast and is a - no-op in fp32 (every cast below is identity when ``x`` is already fp32). The - norm *weights* are kept fp32 on a bf16 standalone load via - ``_keep_in_fp32_modules_strict``; inside ESMFold2 they are bf16-rounded to - match the trained checkpoint and upcast here for the fp32 maths. - """ - - def forward(self, x: torch.Tensor) -> torch.Tensor: - weight = self.weight.float() if self.weight is not None else None - bias = self.bias.float() if self.bias is not None else None - return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) - - class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm (computed in fp32) followed by a Linear projection. + """LayerNorm followed by a Linear projection. Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and - ``weight`` to match the published ESMC checkpoint layout. + ``weight`` to match the published ESMC checkpoint layout. The LayerNorm runs + in the activation dtype (plain ``F.layer_norm``) exactly like the reference: + standalone bf16 → bf16 norm; inside ESMFold2 the backbone is wrapped in a + bf16 autocast (matching the fork's ``_lm_precision_context``), so the norm + then computes in fp32. A no-op in fp32. """ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: @@ -268,10 +253,7 @@ def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: nn.init.normal_(self.weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - # Norm in fp32 (matches the reference), then the projection in x's dtype. - x = F.layer_norm( - x.float(), (self.d_in,), self.layer_norm_weight.float(), self.layer_norm_bias.float(), self.eps - ).to(x.dtype) + x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) return F.linear(x, self.weight) @@ -296,14 +278,13 @@ def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> nn.init.normal_(self.fc2_weight, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: - # Norm in fp32 (matches the reference), then the MLP in x's dtype. x = F.layer_norm( - x.float(), + x, (self.hidden_size,), - self.layer_norm_weight.float(), - self.layer_norm_bias.float(), + self.layer_norm_weight, + self.layer_norm_bias, self.eps, - ).to(x.dtype) + ) x = F.linear(x, self.fc1_weight) x1, x2 = x.chunk(2, dim=-1) x = F.silu(x1) * x2 @@ -379,8 +360,8 @@ def __init__( self.out_proj = _make_attn_out_proj(d_model, bias) if qk_layernorm: - self.q_ln = _Fp32LayerNorm(d_model, bias=bias) - self.k_ln = _Fp32LayerNorm(d_model, bias=bias) + self.q_ln = nn.LayerNorm(d_model, bias=bias) + self.k_ln = nn.LayerNorm(d_model, bias=bias) else: self.q_ln = nn.Identity() self.k_ln = nn.Identity() @@ -560,7 +541,7 @@ def __init__( for _ in range(n_layers) ] ) - self.norm = _Fp32LayerNorm(d_model, bias=False) + self.norm = nn.LayerNorm(d_model, bias=False) def forward( self, @@ -640,15 +621,6 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_attention_backend = True _no_split_modules = ["UnifiedTransformerBlock"] _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] - # ESMC computes its LayerNorms in fp32 (see ``_Fp32LayerNorm``). When the - # model is loaded in bf16 standalone we also keep the norm *weights* in fp32 - # (the standard Transformers idiom). These patterns match only the norm - # parameters — the fused ``layer_norm_weight``/``layer_norm_bias`` plus the - # ``q_ln``/``k_ln`` and final ``transformer.norm`` — never the projection - # weights in the same fused modules. (Inside ESMFold2 the backbone is loaded - # bf16 and the norms bf16-rounded to match the trained checkpoint; see - # ``ESMFold2Model.load_esmc``.) - _keep_in_fp32_modules_strict = ["layer_norm_weight", "layer_norm_bias", "q_ln", "k_ln", "transformer.norm"] def _init_weights(self, module: nn.Module): std = self.config.initializer_range diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index ed986d044398..75f82e723fa0 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -379,22 +379,11 @@ def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: dtype = dtype_map[precision] # Load directly in the target dtype so the ~25GB fp32 6B backbone is never - # materialised (it would not fit alongside the trunk on a 24GB GPU). - esmc = AutoModel.from_pretrained(esmc_model_path, dtype=dtype).to(device=self.device, dtype=dtype) - if dtype == torch.bfloat16: - # The ESMFold2 checkpoint was trained against a bf16-cast backbone, so - # reproduce that exactly. ESMC keeps its norm weights in fp32 on a bf16 - # load (``_keep_in_fp32_modules_strict``); the ``.to(bf16)`` above forced - # those down to bf16, matching the trained rounding. Now upcast just the - # norm weights back to fp32 so the LayerNorms still run in fp32 (the - # reference ran them under autocast). Values stay bf16-rounded — only the - # storage/maths are fp32. Net: bit-identical to "load fp32 -> .to(bf16) -> - # autocast" without ever holding the fp32 backbone. - norm_markers = getattr(esmc, "_keep_in_fp32_modules_strict", None) or () - for name, param in esmc.named_parameters(): - if any(marker in name for marker in norm_markers): - param.data = param.data.float() - esmc = esmc.eval() + # materialised (it would not fit alongside the trunk on a 24GB GPU). The + # backbone is run exactly as the reference does — plain bf16 weights under a + # bf16 autocast (see ``_compute_lm_hidden_states``) — so no per-parameter + # dtype fix-up is needed here. + esmc = AutoModel.from_pretrained(esmc_model_path, dtype=dtype).to(device=self.device, dtype=dtype).eval() for p in esmc.parameters(): p.requires_grad_(False) @@ -462,15 +451,23 @@ def _compute_lm_hidden_states( tok_mask: Tensor, ) -> Tensor: assert self._esmc is not None - return compute_lm_hidden_states( - self._esmc, - input_ids, - asym_id, - residue_index, - mol_type, - tok_mask, - pad_to_multiple=None, - ) + # Run the frozen ESMC backbone exactly as the reference does: plain weights + # under a bf16 autocast (the fork's ``_lm_precision_context``). For a bf16 + # backbone this puts norms/softmax in fp32 and matmuls/rotary in bf16, + # reproducing the checkpoint's training regime bit-for-bit; disabled (a + # no-op) for an fp32 backbone. This autocast is scoped to the backbone only + # — the trunk stays dtype-honest. + use_amp = next(self._esmc.parameters()).dtype == torch.bfloat16 + with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=use_amp): + return compute_lm_hidden_states( + self._esmc, + input_ids, + asym_id, + residue_index, + mol_type, + tok_mask, + pad_to_multiple=None, + ) def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: delta = F.softplus(self.parcae_log_delta) From 8f5b306de068314784038b626a63bfe98de34ca4 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jun 2026 19:20:34 +0100 Subject: [PATCH 36/70] More trunk norm matching --- .../models/esmfold2/modeling_esmfold2.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 75f82e723fa0..678d6549948f 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -397,10 +397,33 @@ def from_pretrained(cls, pretrained_model_name_or_path, *args, load_esmc: bool = # Pop the precision knob before forwarding to the HF loader. esmc_precision = kwargs.pop("esmc_precision", "bf16") model = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs) + # Match the reference's bf16-rounded trunk norm weights (see the method docstring). + # Done before ``load_esmc`` so only the trunk is touched, never the separately + # loaded ESMC backbone (``self._esmc`` is still ``None`` here). + model._round_trunk_norms_to_bf16() if load_esmc: model.load_esmc(model.config.esmc_id, precision=esmc_precision) return model + def _round_trunk_norms_to_bf16(self) -> None: + """Round the trunk LayerNorm weights to bf16 (stored fp32) for a bf16 trunk. + + The trunk LayerNorms are kept fp32 (``nn.LayerNorm(dtype=torch.float32)``) so the + dtype-honest call sites can feed them an upcast input. The reference, however, + loads the whole trunk in bf16 and recovers fp32 only for the norm *compute* (via + autocast) — i.e. its norm *weights* are bf16-ROUNDED. We reproduce that so a bf16 + trunk matches the checkpoint's training regime: round each fp32 norm weight to + bf16 and back. No-op for an fp32 trunk (the fp32 reference keeps full precision), + and the values stay fp32-dtype so the fp32-compute call sites are unaffected. + """ + if not any(p.dtype == torch.bfloat16 for p in self.parameters()): + return + for module in self.modules(): + if isinstance(module, nn.LayerNorm) and module.weight is not None and module.weight.dtype == torch.float32: + module.weight.data = module.weight.data.bfloat16().float() + if module.bias is not None: + module.bias.data = module.bias.data.bfloat16().float() + def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = None) -> None: """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" import torch._dynamo From 561dfc5f386b3c23fc4aea45697b10fea75f7159 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 16:32:50 +0100 Subject: [PATCH 37/70] Big general cleanup, no more _common.py, lots of repo standardization --- docs/source/en/model_doc/esmfold2.md | 19 +- src/transformers/conversion_mapping.py | 10 + .../models/auto/tokenization_auto.py | 1 + .../models/esmc/configuration_esmc.py | 4 +- src/transformers/models/esmc/modeling_esmc.py | 169 +- src/transformers/models/esmc/modular_esmc.py | 141 +- .../models/esmc/tokenization_esmc.py | 3 +- .../models/esmfold2/configuration_esmfold2.py | 28 +- .../models/esmfold2/modeling_esmfold2.py | 2709 +++++++++++++++-- .../esmfold2/modeling_esmfold2_common.py | 2273 -------------- .../models/esmfold2/protein_utils.py | 44 +- tests/models/esmc/test_modeling_esmc.py | 2 +- .../models/esmfold2/test_modeling_esmfold2.py | 117 +- utils/check_repo.py | 2 - 14 files changed, 2838 insertions(+), 2684 deletions(-) delete mode 100644 src/transformers/models/esmfold2/modeling_esmfold2_common.py diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 709d760e4371..83584e874841 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -24,10 +24,10 @@ ESMFold2 is an all-atom protein structure prediction model. It predicts 3D coord backbone. The architecture combines a sliding-window atom encoder with 3D rotary position embeddings, a pairwise folding trunk applied iteratively (the "parcae" recurrence), a diffusion-based structure head, and a confidence head. -The ESMC backbone is **not bundled** in the ESMFold2 checkpoint; it is loaded separately from the repository named by -`config.esmc_id` (default [`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B)). -[`ESMFold2Model.from_pretrained`](#transformers.ESMFold2Model) loads it automatically — pass `load_esmc=False` to skip -this (e.g. when supplying precomputed language-model hidden states). +The ESMC backbone is a bundled submodule: it is built from `config.esmc_config` (defaulting to the ESMC-6B +configuration) and its weights are part of the ESMFold2 checkpoint under `esmc.*`, so a single +[`ESMFold2Model.from_pretrained`](#transformers.ESMFold2Model) loads the whole model — no separate backbone download. +You can still bypass the backbone by supplying precomputed `lm_hidden_states` to `forward`. The model checkpoint is available on the Hugging Face Hub at [`biohub/ESMFold2`](https://huggingface.co/biohub/ESMFold2). @@ -39,7 +39,7 @@ import torch from transformers import ESMFold2Model -# Loading the model also downloads and attaches the ESMC backbone (config.esmc_id). +# The ESMC backbone is bundled in the checkpoint and loaded with the model. # bf16 is the recommended inference precision. model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() @@ -48,8 +48,9 @@ with open("prediction.pdb", "w") as f: f.write(pdb_string) ``` -`infer_protein` returns the raw outputs (atom coordinates, distogram logits and confidence metrics) as a dictionary if -you need them instead of a PDB string. Generation is stochastic — set a manual seed for reproducible structures. +`infer_protein` returns the raw outputs (atom coordinates, distogram logits and confidence metrics) as an +[`~models.esmfold2.modeling_esmfold2.ESMFold2Output`] if you need them instead of a PDB string. Generation is +stochastic — set a manual seed for reproducible structures. ## Faster inference with a fused kernel @@ -75,6 +76,10 @@ pdb_string = model.infer_protein_as_pdb("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ") [[autodoc]] ESMFold2Config +## ESMFold2PreTrainedModel + +[[autodoc]] ESMFold2PreTrainedModel + ## ESMFold2Model [[autodoc]] ESMFold2Model diff --git a/src/transformers/conversion_mapping.py b/src/transformers/conversion_mapping.py index 1b6bcb9d0b6a..0ac43952d6a7 100755 --- a/src/transformers/conversion_mapping.py +++ b/src/transformers/conversion_mapping.py @@ -790,6 +790,16 @@ def _build_checkpoint_conversion_mapping(): "rotary_embeddings.inv_freq", ), ], + # Scoped to the class (not the "esmc" model_type) so it only applies to the + # head model: the published ESMC checkpoints store the masked-LM head as an + # ``nn.Sequential`` (keys ``lm_head.{0,2,3}``); ``ESMCForMaskedLM`` uses a + # named ``ESMCMaskedLMHead`` (``dense`` / ``layer_norm`` / ``decoder``). Remap + # on load (and reverse on save). ``ESMCModel`` (the backbone) has no lm_head. + "ESMCForMaskedLM": [ + WeightRenaming(r"lm_head\.0\.", "lm_head.dense."), + WeightRenaming(r"lm_head\.2\.", "lm_head.layer_norm."), + WeightRenaming(r"lm_head\.3\.", "lm_head.decoder."), + ], "dinov3_convnext": [WeightRenaming(r"(?>> from transformers import ESMCConfig, ESMCModel - >>> # Initializing an ESMC biohub/ESMC-600M style configuration + >>> # Initializing an ESMC biohub/ESMC-6B style configuration >>> configuration = ESMCConfig() >>> # Initializing a model (with random weights) from the configuration diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 211aca27f90b..c6c81582e117 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -174,7 +174,7 @@ def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: def _apply(self, fn, recurse=True): # Keep ``inv_freq`` in fp32 even when the module is cast to bf16/fp16 (e.g. - # ``ESMFold2Model.load_esmc`` does ``.to(bf16)``). ``super()._apply`` would + # when ESMFold2 loads its bundled ESMC backbone in bf16). ``super()._apply`` would # round the rotary frequencies to bf16, which drifts the RoPE angles and is # the dominant source of bf16 fork-vs-port divergence in the ESMFold2 backbone. result = super()._apply(fn, recurse=recurse) @@ -196,8 +196,8 @@ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Te return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) -class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection. +class ESMCLayerNormLinear(nn.Module): + """LayerNorm followed by a Linear projection (the fused QKV projection). Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and ``weight`` to match the published ESMC checkpoint layout. The LayerNorm runs @@ -221,23 +221,34 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return F.linear(x, self.weight) -class _PyTorchLayerNormMLP(nn.Module): - """LayerNorm + SwiGLU MLP. +# --------------------------------------------------------------------------- +# Feed-forward network helpers +# --------------------------------------------------------------------------- + + +def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: + """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" + return int(((expansion_ratio * d_model) + 255) // 256 * 256) + + +class ESMCSwiGLUMLP(nn.Module): + """LayerNorm + SwiGLU feed-forward network. Parameters are named ``layer_norm_weight``, ``layer_norm_bias``, ``fc1_weight`` and ``fc2_weight`` to match the published ESMC checkpoint - layout. + layout. ESMC was trained with ``bias=False``, so this network has no biases. """ - def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> None: + def __init__(self, d_model: int, expansion_ratio: float, eps: float = 1e-5) -> None: super().__init__() - self.hidden_size = hidden_size + ffn_hidden_size = _swiglu_hidden_dim(expansion_ratio, d_model) + self.hidden_size = d_model self.ffn_hidden_size = ffn_hidden_size self.eps = eps - self.layer_norm_weight = nn.Parameter(torch.ones(hidden_size)) - self.layer_norm_bias = nn.Parameter(torch.zeros(hidden_size)) - self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, hidden_size)) - self.fc2_weight = nn.Parameter(torch.empty(hidden_size, ffn_hidden_size)) + self.layer_norm_weight = nn.Parameter(torch.ones(d_model)) + self.layer_norm_bias = nn.Parameter(torch.zeros(d_model)) + self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, d_model)) + self.fc2_weight = nn.Parameter(torch.empty(d_model, ffn_hidden_size)) nn.init.normal_(self.fc1_weight, std=0.02) nn.init.normal_(self.fc2_weight, std=0.02) @@ -255,6 +266,20 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return F.linear(x, self.fc2_weight) +class ESMCGeluMLP(nn.Module): + """LayerNorm + GELU feed-forward network (the non-default ``ffn_type='gelu'``).""" + + def __init__(self, d_model: int, expansion_ratio: float, bias: bool = False) -> None: + super().__init__() + hidden = int(expansion_ratio * d_model) + self.layer_norm = nn.LayerNorm(d_model) + self.fc1 = nn.Linear(d_model, hidden, bias=bias) + self.fc2 = nn.Linear(hidden, d_model, bias=bias) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.fc2(F.gelu(self.fc1(self.layer_norm(x)))) + + def rotate_half(x): """Rotates half the hidden dims of the input.""" x1 = x[..., : x.shape[-1] // 2] @@ -310,23 +335,12 @@ def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): return q_embed, k_embed -def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: - """LayerNorm + fused QKV projection.""" - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - return _PyTorchLayerNormLinear(d_model, d_model * 3) - - -def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: - """Attention output projection.""" - return nn.Linear(d_model, d_model, bias=bias) - - # --------------------------------------------------------------------------- # Attention # --------------------------------------------------------------------------- -class MultiHeadAttention(nn.Module): +class ESMCAttention(nn.Module): """Multi-head self-attention with QK LayerNorm and RoPE. Args: @@ -346,6 +360,8 @@ def __init__( qk_layernorm: bool = True, ): super().__init__() + if bias: + raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") self.config = config self.d_model = d_model self.n_heads = n_heads @@ -357,9 +373,8 @@ def __init__( # attention_mask is passed (unpadded inputs), silently masking causally. self.is_causal = False - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) - self.out_proj = _make_attn_out_proj(d_model, bias) + self.layernorm_qkv = ESMCLayerNormLinear(d_model, d_model * 3) + self.out_proj = nn.Linear(d_model, d_model, bias=bias) if qk_layernorm: self.q_ln = nn.LayerNorm(d_model, bias=bias) @@ -421,39 +436,12 @@ def forward( return self.out_proj(attn_output), attn_weights -# --------------------------------------------------------------------------- -# Feed-forward network helpers -# --------------------------------------------------------------------------- - - -def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: - """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" - return int(((expansion_ratio * d_model) + 255) // 256 * 256) - - -def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: - """LayerNorm + SwiGLU MLP.""" - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - hidden = _swiglu_hidden_dim(expansion_ratio, d_model) - return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) - - -def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequential: - hidden = int(expansion_ratio * d_model) - return nn.Sequential( - nn.LayerNorm(d_model), - nn.Linear(d_model, hidden, bias=bias), - nn.GELU(), - nn.Linear(hidden, d_model, bias=bias), - ) - - # --------------------------------------------------------------------------- # Transformer blocks # --------------------------------------------------------------------------- -class UnifiedTransformerBlock(nn.Module): +class ESMCLayer(nn.Module): """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. Args: @@ -480,13 +468,15 @@ def __init__( ffn_type: str = "swiglu", ): super().__init__() + if bias: + raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") - self.attn = MultiHeadAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + self.attn = ESMCAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) if ffn_type == "swiglu": - self.ffn = _swiglu_ln_ffn(d_model, expansion_ratio, bias) + self.ffn = ESMCSwiGLUMLP(d_model, expansion_ratio) elif ffn_type == "gelu": - self.ffn = _gelu_ln_ffn(d_model, expansion_ratio, bias) + self.ffn = ESMCGeluMLP(d_model, expansion_ratio, bias) else: raise ValueError(f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'.") @@ -526,8 +516,8 @@ def forward( return x, attn_weights -class TransformerStack(nn.Module): - """Stack of :class:`UnifiedTransformerBlock` layers with a final LayerNorm. +class ESMCEncoder(nn.Module): + """Stack of :class:`ESMCLayer` layers with a final LayerNorm. Args: d_model: Hidden dimension. @@ -557,7 +547,7 @@ def __init__( super().__init__() self.blocks = nn.ModuleList( [ - UnifiedTransformerBlock( + ESMCLayer( config, d_model, n_heads, @@ -648,7 +638,7 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_sdpa = True _supports_flash_attn = True _supports_attention_backend = True - _no_split_modules = ["UnifiedTransformerBlock"] + _no_split_modules = ["ESMCLayer"] _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] def _init_weights(self, module: nn.Module): @@ -656,11 +646,11 @@ def _init_weights(self, module: nn.Module): # The fused LN+projection modules are handled explicitly: the base # `_init_weights` matches norms by class-name substring ("LayerNorm"), # which would otherwise mis-initialize their (Linear) `weight` to ones. - if isinstance(module, _PyTorchLayerNormLinear): + if isinstance(module, ESMCLayerNormLinear): init.ones_(module.layer_norm_weight) init.zeros_(module.layer_norm_bias) init.normal_(module.weight, mean=0.0, std=std) - elif isinstance(module, _PyTorchLayerNormMLP): + elif isinstance(module, ESMCSwiGLUMLP): init.ones_(module.layer_norm_weight) init.zeros_(module.layer_norm_bias) init.normal_(module.fc1_weight, mean=0.0, std=std) @@ -694,7 +684,7 @@ def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) self.rotary_emb = ESMCRotaryEmbedding(config) - self.transformer = TransformerStack( + self.transformer = ESMCEncoder( config, config.d_model, config.n_heads, @@ -730,7 +720,7 @@ def forward( output_attentions (`bool`, *optional*): Whether to return the per-block attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. - Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the + Forces a manual-SDPA path inside :class:`ESMCAttention` so the attention probabilities are observable; raises on the ``flash_attention_2`` path. @@ -739,8 +729,8 @@ def forward( ```python >>> from transformers import AutoTokenizer, ESMCModel - >>> model = ESMCModel.from_pretrained("Biohub/ESMC-600M-2024-12") - >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> model = ESMCModel.from_pretrained("biohub/ESMC-300M") + >>> tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") >>> inputs = tokenizer(["MLKNVQVQLV"], return_tensors="pt") >>> outputs = model(**inputs) >>> outputs.last_hidden_state.shape @@ -823,15 +813,26 @@ def forward( # --------------------------------------------------------------------------- -def _esmc_lm_head(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Sequential: - """Linear → GELU → LayerNorm → Linear projection head for masked LM.""" - hidden_dim = hidden_dim if hidden_dim is not None else d_model - return nn.Sequential( - nn.Linear(d_model, hidden_dim), - nn.GELU(), - nn.LayerNorm(hidden_dim), - nn.Linear(hidden_dim, output_dim), - ) +class ESMCMaskedLMHead(nn.Module): + """Masked-language-modelling head: Linear → GELU → LayerNorm → Linear. + + The published checkpoints store this head as an ``nn.Sequential`` (keys + ``lm_head.{0,2,3}``); the ``esmc`` entry in ``conversion_mapping.py`` remaps + them onto ``dense`` / ``layer_norm`` / ``decoder`` on load (and back on save). + """ + + def __init__(self, d_model: int, output_dim: int, hidden_dim: int | None = None) -> None: + super().__init__() + hidden_dim = hidden_dim if hidden_dim is not None else d_model + self.dense = nn.Linear(d_model, hidden_dim) + self.layer_norm = nn.LayerNorm(hidden_dim) + self.decoder = nn.Linear(hidden_dim, output_dim) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + hidden_states = self.dense(hidden_states) + hidden_states = F.gelu(hidden_states) + hidden_states = self.layer_norm(hidden_states) + return self.decoder(hidden_states) # --------------------------------------------------------------------------- @@ -851,14 +852,14 @@ class ESMCForMaskedLM(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) self.esmc = ESMCModel(config) - self.lm_head = _esmc_lm_head(config.d_model, config.vocab_size) + self.lm_head = ESMCMaskedLMHead(config.d_model, config.vocab_size) self.post_init() def get_output_embeddings(self) -> nn.Linear: - return self.lm_head[-1] # type: ignore[return-value] + return self.lm_head.decoder def set_output_embeddings(self, new_embeddings: nn.Linear): - self.lm_head[-1] = new_embeddings + self.lm_head.decoder = new_embeddings @can_return_tuple @auto_docstring @@ -889,8 +890,8 @@ def forward( >>> from transformers import AutoTokenizer, ESMCForMaskedLM >>> import torch - >>> model = ESMCForMaskedLM.from_pretrained("Biohub/ESMC-600M-2024-12") - >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> model = ESMCForMaskedLM.from_pretrained("biohub/ESMC-300M") + >>> tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") >>> inputs = tokenizer(["MLKNVQLV"], return_tensors="pt") >>> outputs = model(**inputs) >>> outputs.logits.shape @@ -941,7 +942,7 @@ def forward( # --------------------------------------------------------------------------- -class _ESMCClassificationHead(nn.Module): +class ESMCClassificationHead(nn.Module): """Dense classification head applied to the ```` token representation.""" def __init__(self, config: ESMCConfig): @@ -976,7 +977,7 @@ def __init__(self, config: ESMCConfig): super().__init__(config) self.num_labels = config.num_labels self.esmc = ESMCModel(config) - self.classifier = _ESMCClassificationHead(config) + self.classifier = ESMCClassificationHead(config) self.post_init() @can_return_tuple diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 381392694b26..26a391aef17b 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -180,7 +180,7 @@ def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: def _apply(self, fn, recurse=True): # Keep ``inv_freq`` in fp32 even when the module is cast to bf16/fp16 (e.g. - # ``ESMFold2Model.load_esmc`` does ``.to(bf16)``). ``super()._apply`` would + # when ESMFold2 loads its bundled ESMC backbone in bf16). ``super()._apply`` would # round the rotary frequencies to bf16, which drifts the RoPE angles and is # the dominant source of bf16 fork-vs-port divergence in the ESMFold2 backbone. result = super()._apply(fn, recurse=recurse) @@ -232,8 +232,8 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: return int(((expansion_ratio * d_model) + 255) // 256 * 256) -class _PyTorchLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection. +class ESMCLayerNormLinear(nn.Module): + """LayerNorm followed by a Linear projection (the fused QKV projection). Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and ``weight`` to match the published ESMC checkpoint layout. The LayerNorm runs @@ -257,23 +257,24 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return F.linear(x, self.weight) -class _PyTorchLayerNormMLP(nn.Module): - """LayerNorm + SwiGLU MLP. +class ESMCSwiGLUMLP(nn.Module): + """LayerNorm + SwiGLU feed-forward network. Parameters are named ``layer_norm_weight``, ``layer_norm_bias``, ``fc1_weight`` and ``fc2_weight`` to match the published ESMC checkpoint - layout. + layout. ESMC was trained with ``bias=False``, so this network has no biases. """ - def __init__(self, hidden_size: int, ffn_hidden_size: int, eps: float = 1e-5) -> None: + def __init__(self, d_model: int, expansion_ratio: float, eps: float = 1e-5) -> None: super().__init__() - self.hidden_size = hidden_size + ffn_hidden_size = _swiglu_hidden_dim(expansion_ratio, d_model) + self.hidden_size = d_model self.ffn_hidden_size = ffn_hidden_size self.eps = eps - self.layer_norm_weight = nn.Parameter(torch.ones(hidden_size)) - self.layer_norm_bias = nn.Parameter(torch.zeros(hidden_size)) - self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, hidden_size)) - self.fc2_weight = nn.Parameter(torch.empty(hidden_size, ffn_hidden_size)) + self.layer_norm_weight = nn.Parameter(torch.ones(d_model)) + self.layer_norm_bias = nn.Parameter(torch.zeros(d_model)) + self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, d_model)) + self.fc2_weight = nn.Parameter(torch.empty(d_model, ffn_hidden_size)) nn.init.normal_(self.fc1_weight, std=0.02) nn.init.normal_(self.fc2_weight, std=0.02) @@ -291,32 +292,18 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return F.linear(x, self.fc2_weight) -def _swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module: - """LayerNorm + SwiGLU MLP.""" - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - hidden = _swiglu_hidden_dim(expansion_ratio, d_model) - return _PyTorchLayerNormMLP(hidden_size=d_model, ffn_hidden_size=hidden) - - -def _make_attn_layernorm_qkv(d_model: int, bias: bool) -> nn.Module: - """LayerNorm + fused QKV projection.""" - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - return _PyTorchLayerNormLinear(d_model, d_model * 3) - - -def _make_attn_out_proj(d_model: int, bias: bool) -> nn.Module: - """Attention output projection.""" - return nn.Linear(d_model, d_model, bias=bias) +class ESMCGeluMLP(nn.Module): + """LayerNorm + GELU feed-forward network (the non-default ``ffn_type='gelu'``).""" + def __init__(self, d_model: int, expansion_ratio: float, bias: bool = False) -> None: + super().__init__() + hidden = int(expansion_ratio * d_model) + self.layer_norm = nn.LayerNorm(d_model) + self.fc1 = nn.Linear(d_model, hidden, bias=bias) + self.fc2 = nn.Linear(hidden, d_model, bias=bias) -def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequential: - hidden = int(expansion_ratio * d_model) - return nn.Sequential( - nn.LayerNorm(d_model), - nn.Linear(d_model, hidden, bias=bias), - nn.GELU(), - nn.Linear(hidden, d_model, bias=bias), - ) + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.fc2(F.gelu(self.fc1(self.layer_norm(x)))) # --------------------------------------------------------------------------- @@ -324,7 +311,7 @@ def _gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Sequent # --------------------------------------------------------------------------- -class MultiHeadAttention(nn.Module): +class ESMCAttention(nn.Module): """Multi-head self-attention with QK LayerNorm and RoPE. Args: @@ -344,6 +331,8 @@ def __init__( qk_layernorm: bool = True, ): super().__init__() + if bias: + raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") self.config = config self.d_model = d_model self.n_heads = n_heads @@ -355,9 +344,8 @@ def __init__( # attention_mask is passed (unpadded inputs), silently masking causally. self.is_causal = False - assert not bias, "ESMC was trained with bias=False; bias=True not supported" - self.layernorm_qkv = _make_attn_layernorm_qkv(d_model, bias) - self.out_proj = _make_attn_out_proj(d_model, bias) + self.layernorm_qkv = ESMCLayerNormLinear(d_model, d_model * 3) + self.out_proj = nn.Linear(d_model, d_model, bias=bias) if qk_layernorm: self.q_ln = nn.LayerNorm(d_model, bias=bias) @@ -424,7 +412,7 @@ def forward( # --------------------------------------------------------------------------- -class UnifiedTransformerBlock(nn.Module): +class ESMCLayer(nn.Module): """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. Args: @@ -451,13 +439,15 @@ def __init__( ffn_type: str = "swiglu", ): super().__init__() + if bias: + raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") - self.attn = MultiHeadAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + self.attn = ESMCAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) if ffn_type == "swiglu": - self.ffn = _swiglu_ln_ffn(d_model, expansion_ratio, bias) + self.ffn = ESMCSwiGLUMLP(d_model, expansion_ratio) elif ffn_type == "gelu": - self.ffn = _gelu_ln_ffn(d_model, expansion_ratio, bias) + self.ffn = ESMCGeluMLP(d_model, expansion_ratio, bias) else: raise ValueError(f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'.") @@ -497,8 +487,8 @@ def forward( return x, attn_weights -class TransformerStack(nn.Module): - """Stack of :class:`UnifiedTransformerBlock` layers with a final LayerNorm. +class ESMCEncoder(nn.Module): + """Stack of :class:`ESMCLayer` layers with a final LayerNorm. Args: d_model: Hidden dimension. @@ -528,7 +518,7 @@ def __init__( super().__init__() self.blocks = nn.ModuleList( [ - UnifiedTransformerBlock( + ESMCLayer( config, d_model, n_heads, @@ -619,7 +609,7 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_sdpa = True _supports_flash_attn = True _supports_attention_backend = True - _no_split_modules = ["UnifiedTransformerBlock"] + _no_split_modules = ["ESMCLayer"] _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] def _init_weights(self, module: nn.Module): @@ -627,11 +617,11 @@ def _init_weights(self, module: nn.Module): # The fused LN+projection modules are handled explicitly: the base # `_init_weights` matches norms by class-name substring ("LayerNorm"), # which would otherwise mis-initialize their (Linear) `weight` to ones. - if isinstance(module, _PyTorchLayerNormLinear): + if isinstance(module, ESMCLayerNormLinear): init.ones_(module.layer_norm_weight) init.zeros_(module.layer_norm_bias) init.normal_(module.weight, mean=0.0, std=std) - elif isinstance(module, _PyTorchLayerNormMLP): + elif isinstance(module, ESMCSwiGLUMLP): init.ones_(module.layer_norm_weight) init.zeros_(module.layer_norm_bias) init.normal_(module.fc1_weight, mean=0.0, std=std) @@ -665,7 +655,7 @@ def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) self.rotary_emb = ESMCRotaryEmbedding(config) - self.transformer = TransformerStack( + self.transformer = ESMCEncoder( config, config.d_model, config.n_heads, @@ -701,7 +691,7 @@ def forward( output_attentions (`bool`, *optional*): Whether to return the per-block attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. - Forces a manual-SDPA path inside :class:`MultiHeadAttention` so the + Forces a manual-SDPA path inside :class:`ESMCAttention` so the attention probabilities are observable; raises on the ``flash_attention_2`` path. @@ -710,8 +700,8 @@ def forward( ```python >>> from transformers import AutoTokenizer, ESMCModel - >>> model = ESMCModel.from_pretrained("Biohub/ESMC-600M-2024-12") - >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> model = ESMCModel.from_pretrained("biohub/ESMC-300M") + >>> tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") >>> inputs = tokenizer(["MLKNVQVQLV"], return_tensors="pt") >>> outputs = model(**inputs) >>> outputs.last_hidden_state.shape @@ -794,15 +784,26 @@ def forward( # --------------------------------------------------------------------------- -def _esmc_lm_head(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Sequential: - """Linear → GELU → LayerNorm → Linear projection head for masked LM.""" - hidden_dim = hidden_dim if hidden_dim is not None else d_model - return nn.Sequential( - nn.Linear(d_model, hidden_dim), - nn.GELU(), - nn.LayerNorm(hidden_dim), - nn.Linear(hidden_dim, output_dim), - ) +class ESMCMaskedLMHead(nn.Module): + """Masked-language-modelling head: Linear → GELU → LayerNorm → Linear. + + The published checkpoints store this head as an ``nn.Sequential`` (keys + ``lm_head.{0,2,3}``); the ``esmc`` entry in ``conversion_mapping.py`` remaps + them onto ``dense`` / ``layer_norm`` / ``decoder`` on load (and back on save). + """ + + def __init__(self, d_model: int, output_dim: int, hidden_dim: int | None = None) -> None: + super().__init__() + hidden_dim = hidden_dim if hidden_dim is not None else d_model + self.dense = nn.Linear(d_model, hidden_dim) + self.layer_norm = nn.LayerNorm(hidden_dim) + self.decoder = nn.Linear(hidden_dim, output_dim) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + hidden_states = self.dense(hidden_states) + hidden_states = F.gelu(hidden_states) + hidden_states = self.layer_norm(hidden_states) + return self.decoder(hidden_states) # --------------------------------------------------------------------------- @@ -822,14 +823,14 @@ class ESMCForMaskedLM(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) self.esmc = ESMCModel(config) - self.lm_head = _esmc_lm_head(config.d_model, config.vocab_size) + self.lm_head = ESMCMaskedLMHead(config.d_model, config.vocab_size) self.post_init() def get_output_embeddings(self) -> nn.Linear: - return self.lm_head[-1] # type: ignore[return-value] + return self.lm_head.decoder def set_output_embeddings(self, new_embeddings: nn.Linear): - self.lm_head[-1] = new_embeddings + self.lm_head.decoder = new_embeddings @can_return_tuple @auto_docstring @@ -860,8 +861,8 @@ def forward( >>> from transformers import AutoTokenizer, ESMCForMaskedLM >>> import torch - >>> model = ESMCForMaskedLM.from_pretrained("Biohub/ESMC-600M-2024-12") - >>> tokenizer = AutoTokenizer.from_pretrained("Biohub/ESMC-600M-2024-12") + >>> model = ESMCForMaskedLM.from_pretrained("biohub/ESMC-300M") + >>> tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") >>> inputs = tokenizer(["MLKNVQLV"], return_tensors="pt") >>> outputs = model(**inputs) >>> outputs.logits.shape @@ -912,7 +913,7 @@ def forward( # --------------------------------------------------------------------------- -class _ESMCClassificationHead(nn.Module): +class ESMCClassificationHead(nn.Module): """Dense classification head applied to the ```` token representation.""" def __init__(self, config: ESMCConfig): @@ -947,7 +948,7 @@ def __init__(self, config: ESMCConfig): super().__init__(config) self.num_labels = config.num_labels self.esmc = ESMCModel(config) - self.classifier = _ESMCClassificationHead(config) + self.classifier = ESMCClassificationHead(config) self.post_init() @can_return_tuple diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index 3c1f65e8f21a..af66b2d771d1 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -187,7 +187,8 @@ def chain_break_token(self) -> str: @property def chain_break_token_id(self) -> int: token_id = self.convert_tokens_to_ids(self._chain_break_token) - assert isinstance(token_id, int) + if not isinstance(token_id, int): + raise TypeError(f"Expected a single token id for the chain-break token, got {token_id!r}.") return token_id # ------------------------------------------------------------------ diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 46af732d5745..acdacd6c7309 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -17,12 +17,11 @@ from ...configuration_utils import PreTrainedConfig from ...utils import auto_docstring, logging +from ..esmc.configuration_esmc import ESMCConfig logger = logging.get_logger(__name__) -_DEFAULT_ESMC_HF_REPO = "biohub/ESMC-6B" - # --------------------------------------------------------------------------- # Nested sub-configs (registered via the parent's ``sub_configs``) @@ -46,9 +45,6 @@ class ParcaeConfig(PreTrainedConfig): """Release-only config for the parcae diffusion-loop scheduler.""" enabled: bool | None = True - poisson_mean: float | None = 3.0 - min_steps: int | None = 1 - max_steps: int | None = 6 coda_n_layers: int | None = 2 @@ -114,8 +110,6 @@ class DiffusionModuleConfig(PreTrainedConfig): c_z: int | None = 256 c_s_inputs: int | None = 451 fourier_dim: int | None = 256 - relpos_r_max: int | None = 32 - relpos_s_max: int | None = 2 atom_num_blocks: int | None = 3 atom_num_heads: int | None = 4 token_num_blocks: int | None = 12 @@ -131,9 +125,6 @@ class DiffusionStructureHeadConfig(PreTrainedConfig): diffusion_module: dict | DiffusionModuleConfig | None = None distogram_bins: int | None = 128 - # Training noise: sigma ~ sigma_data * exp(mu + sigma * N(0,1)) - train_noise_log_mean: float | None = -1.2 - train_noise_log_std: float | None = 1.5 # Sampling defaults (ODE) gamma_0: float | None = 0.605 gamma_min: float | None = 1.107 @@ -200,13 +191,10 @@ class ESMFold2Config(PreTrainedConfig): Number of trunk loops for iterative refinement. num_diffusion_samples (`int`, *optional*, defaults to 8): Number of parallel structure predictions to generate. - lm_d_model (`int`, *optional*, defaults to 2560): - Hidden size of the ESMC language-model backbone. - lm_num_layers (`int`, *optional*, defaults to 80): - Number of layers in the ESMC language-model backbone. - esmc_id (`str`, *optional*, defaults to `"biohub/ESMC-6B"`): - Hub id of the ESMC backbone, loaded separately (the ESMFold2 checkpoint - does not bundle ESMC weights). + esmc_config (`ESMCConfig`, *optional*): + Configuration of the bundled ESMC language-model backbone. Defaults to the + ESMC-6B configuration. The backbone weights are part of the ESMFold2 + checkpoint (built with `AutoModel.from_config(esmc_config)`). msa_encoder_overwrite (`bool`, *optional*, defaults to `True`): If `True`, MSA encoder output replaces the pair stream; if `False`, it is added. @@ -250,6 +238,7 @@ class ESMFold2Config(PreTrainedConfig): "msa_encoder": MSAEncoderConfig, "parcae": ParcaeConfig, "lm_encoder": LMEncoderConfig, + "esmc_config": ESMCConfig, } type: str | None = "release" @@ -259,9 +248,7 @@ class ESMFold2Config(PreTrainedConfig): n_relative_chain_bins: int | None = 2 num_loops: int | None = 10 num_diffusion_samples: int | None = 8 - lm_d_model: int | None = 2560 - lm_num_layers: int | None = 80 - esmc_id: str | None = _DEFAULT_ESMC_HF_REPO + esmc_config: dict | ESMCConfig | None = None msa_encoder_overwrite: bool | None = True inputs: dict | InputsEmbedderConfig | None = None folding_trunk: dict | FoldingTrunkConfig | None = None @@ -292,6 +279,7 @@ def _init_nested(cls, val): self.msa_encoder = _init_nested(MSAEncoderConfig, self.msa_encoder) self.parcae = _init_nested(ParcaeConfig, self.parcae) self.lm_encoder = _init_nested(LMEncoderConfig, self.lm_encoder) + self.esmc_config = _init_nested(ESMCConfig, self.esmc_config) super().__post_init__(**kwargs) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 678d6549948f..14931974c735 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -1,3 +1,16 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """PyTorch ESMFold2 model — the standard released architecture. Quickstart:: @@ -11,48 +24,2392 @@ companion ``esm`` package. """ +from __future__ import annotations + import math +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor +from torch.utils.checkpoint import checkpoint + + +try: + from flash_attn import ( + flash_attn_func, + flash_attn_varlen_func, + ) + from flash_attn.bert_padding import ( + index_first_axis, + pad_input, + ) + + FLASH_ATTN_AVAILABLE = True +except ImportError: + flash_attn_func = None + flash_attn_varlen_func = None + index_first_axis = None + pad_input = None + FLASH_ATTN_AVAILABLE = False + +from ... import initialization as init +from ...integrations import use_kernel_forward_from_hub +from ...modeling_outputs import ModelOutput +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from ...processing_utils import Unpack +from ...utils import TransformersKwargs +from ..auto import AutoModel +from .configuration_esmfold2 import ESMFold2Config + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +CHAR_VOCAB_SIZE: int = 64 +MAX_CHARS: int = 4 +XYZ_DIMS: int = 3 +MAX_ATOMIC_NUMBER: int = 128 + +# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 +ATOM_FEATURE_DIM: int = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS + + +NUM_RES_TYPES: int = 33 + +_EPS = 1e-5 + +# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory +# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; +# chunk=64 leaves headroom for the largest foldbench targets). Override via +# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for +# short L but OOM-prone past ~600). +_DEFAULT_CHUNK_SIZE = 64 + + +# =========================================================================== +# Atom-token utilities +# =========================================================================== + + +def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: + """Broadcast per-token features to per-atom features using gather. + + Args: + token_features: [B, L, d] + atom_to_token_idx: [B, A] int64 + + Returns: + [B, A, d] + """ + idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) + return torch.gather(token_features, 1, idx) + + +def scatter_atom_to_token( + atom_features: Tensor, + atom_to_token_idx: Tensor, + n_tokens: int, + atom_mask: Tensor | None = None, +) -> Tensor: + """Aggregate per-atom features to per-token features (mean). + + Args: + atom_features: [B, A, d] + atom_to_token_idx: [B, A] int64 + n_tokens: L + atom_mask: [B, A] bool + + Returns: + [B, L, d] + """ + B, A, d = atom_features.shape + n_out = n_tokens + idx = atom_to_token_idx + if atom_mask is not None: + idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) + n_out = n_tokens + 1 + idx_expanded = idx.unsqueeze(-1).expand(B, A, d) + out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) + out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) + return out[:, :n_tokens, :] + + +def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: + """Gather representative atom coordinates for each token. + + Args: + coords: [B, A, 3] + rep_atom_idx: [B, L] int64 + + Returns: + [B, L, 3] + """ + idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) + return torch.gather(coords, 1, idx) + + +def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: + """Compute local atom index within each token (vectorised). + + Atoms belonging to the same token are contiguous, so this computes a + running count that resets at each token boundary. + + Args: + atom_to_token: [B, A] flat index mapping each atom to its token. + + Returns: + [B, A] tensor with values in [0, max_atoms_per_token - 1]. + """ + same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) + ones = torch.ones_like(atom_to_token) + cumsum = torch.cumsum(ones, dim=-1) + group_start = cumsum.masked_fill(same_as_prev, 0) + group_start = torch.cummax(group_start, dim=-1).values + return cumsum - group_start + + +def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: + """Expected value of a categorical distribution over evenly-spaced bins. + + Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. + + Args: + logits: [..., n_bins] + start: left boundary + end: right boundary + + Returns: + [...] expected value + """ + n_bins = logits.shape[-1] + edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) + v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] + return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) + + +# =========================================================================== +# fp32-compute LayerNorm +# =========================================================================== + + +class ESMFold2LayerNorm(nn.LayerNorm): + """LayerNorm that always computes in fp32, with its weight stored at the model dtype. + + ESMFold2 runs in the loaded dtype (bf16 for inference) but its LayerNorms must match + the reference's fp32 norm *compute* (the fork upcasts via autocast). Keeping the weight + at the model dtype means ``from_pretrained(dtype=bf16)`` rounds it to bf16 exactly like + the reference's bf16-trained norm weights, while this ``forward`` upcasts to fp32 for + the computation and casts the result back — so no post-load weight fix-up is needed + (an fp32 load keeps full-precision weights and an fp32 compute, both no-ops here). + """ + + def forward(self, x: Tensor) -> Tensor: + weight = self.weight.float() if self.weight is not None else None + bias = self.bias.float() if self.bias is not None else None + return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) + + +# =========================================================================== +# ESMFold2TransitionLayer (used in ESMFold2DiffusionConditioning) +# =========================================================================== + + +class ESMFold2TransitionLayer(nn.Module): + """ESMFold2SwiGLU transition: norm -> a_proj, b_proj -> silu(a)*b -> out_proj.""" + + def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: + super().__init__() + hidden = n * d_model + self.norm = ESMFold2LayerNorm(d_model, eps=eps) + self.a_proj = nn.Linear(d_model, hidden, bias=False) + self.b_proj = nn.Linear(d_model, hidden, bias=False) + self.out_proj = nn.Linear(hidden, d_model, bias=False) + + def forward(self, x: Tensor) -> Tensor: + x = self.norm(x.float()).to(x.dtype) + a = self.a_proj(x) + b = self.b_proj(x) + return self.out_proj(F.silu(a) * b) + + +# =========================================================================== +# ESMFold2AdaptiveLayerNorm (used in ESMFold2DiffusionTransformer) +# =========================================================================== + + +class ESMFold2AdaptiveLayerNorm(nn.Module): + """Adaptive layer normalization (adaLN-Zero).""" + + def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: + super().__init__() + self.d_model = d_model + self.d_cond = d_cond + self.eps = eps + self.s_scale = nn.Parameter(torch.empty(d_cond)) # ones-init in _init_weights + self.s_gate = nn.Linear(d_cond, d_model, bias=True) + self.s_shift = nn.Linear(d_cond, d_model, bias=False) + + def forward(self, a: Tensor, s: Tensor) -> Tensor: + a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) + s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) + # gate/shift in bf16 (matches the reference's autocast); a_norm is fp32 so the + # affine promotes to fp32, then downcast for the next op. + gate = torch.sigmoid(self.s_gate(s_norm)) + shift = self.s_shift(s_norm) + return (gate * a_norm + shift).to(a.dtype) + + +# =========================================================================== +# ESMFold2FourierEmbedding +# =========================================================================== + + +class ESMFold2FourierEmbedding(nn.Module): + """Fourier embedding: cos(2*pi*(t*w + b)).""" + + w: Tensor + b: Tensor + + def __init__(self, c: int) -> None: + super().__init__() + self.c = c + self.register_buffer("w", torch.randn(c)) + self.register_buffer("b", torch.randn(c)) + + def forward(self, t_hat: Tensor) -> Tensor: + # w/b are kept fp32 (ESMFold2Model._keep_in_fp32_modules_strict), so the random + # frequencies/phases — and the cos embedding — are computed at full precision. + t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) + return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) + + +# =========================================================================== +# ESMFold2SwiGLU / ESMFold2SwiGLUMLP +# =========================================================================== + + +class ESMFold2SwiGLU(nn.Module): + """ESMFold2SwiGLU with packed w12 and output w3.""" + + def __init__( + self, + in_features: int, + hidden_features: int, + out_features: int | None = None, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) + self.w3 = nn.Linear(hidden_features, out_features, bias=bias) + self.hidden_features = hidden_features + + def forward(self, x: Tensor) -> Tensor: + x12 = self.w12(x) + x1, x2 = x12.split(self.hidden_features, dim=-1) + hidden = F.silu(x1) * x2 + return self.w3(hidden) + + +class ESMFold2SwiGLUMLP(ESMFold2SwiGLU): + """ESMFold2SwiGLU MLP with packed weights, no bias.""" + + def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: + hidden = expansion_ratio * d_model + super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) + + +# =========================================================================== +# SWA Atom Attention components +# =========================================================================== + + +# Copied from transformers.models.esm.modeling_esm.rotate_half +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) + + +def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: + """Apply RoPE with batch-dependent cos/sin. + + Args: + x: [B, L, H, D] + cos: [B, L, D/2] + sin: [B, L, D/2] + """ + ro_dim = cos.shape[-1] * 2 + cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) + sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) + return torch.cat( + [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], + dim=-1, + ) + + +@torch.compiler.disable +def build_3d_rope( + ref_pos: Tensor, + ref_space_uid: Tensor, + head_dim: int, + n_spatial_per_axis: int = 4, + n_uid_pairs: int = 2, + spatial_base_freq: float = 10000.0, + uid_base_freq: float = 10.0, +) -> tuple[Tensor, Tensor]: + """Build cos/sin for 3D RoPE + UID RoPE.""" + device = ref_pos.device + B, N = ref_pos.shape[:2] + half_dim = head_dim // 2 + n_spatial_total = 3 * n_spatial_per_axis + + spatial_inv_freq = 1.0 / ( + spatial_base_freq + ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) + ) + uid_inv_freq = 1.0 / ( + uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) + ) + + pos_f32 = ref_pos.float() + spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) + spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) + + uid_f32 = ref_space_uid.float() + uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) + + n_active = n_spatial_total + n_uid_pairs + freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) + + if n_active < half_dim: + padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) + freqs = torch.cat([freqs, padding], dim=-1) + + cos = freqs.cos().to(torch.bfloat16) + sin = freqs.sin().to(torch.bfloat16) + return cos, sin + + +def qk_norm(x: Tensor) -> Tensor: + return F.rms_norm(x, (x.size(-1),)) + + +# =========================================================================== +# ESMFold2SwiGLUFFN (atom transformer blocks) +# =========================================================================== + + +class ESMFold2SwiGLUFFN(nn.Module): + """ESMFold2SwiGLU FFN with rounded hidden size for hardware alignment.""" + + def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: + super().__init__() + hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 + self.w_up = nn.Linear(d_model, 2 * hidden_size, bias=False) + self.w_down = nn.Linear(hidden_size, d_model, bias=False) + + def forward(self, x: Tensor) -> Tensor: + x = x.to(self.w_up.weight.dtype) + x1, x2 = self.w_up(x).chunk(2, dim=-1) + return self.w_down(F.silu(x1) * x2) + + +# =========================================================================== +# ESMFold2SWA3DRoPEAttention +# =========================================================================== + + +# Copied from transformers.models.llama.modeling_llama.repeat_kv +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim) + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) + + +# Copied from transformers.models.llama.modeling_llama.eager_attention_forward +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: torch.Tensor | None, + scaling: float, + dropout: float = 0.0, + **kwargs: Unpack[TransformersKwargs], +): + key_states = repeat_kv(key, module.num_key_value_groups) + value_states = repeat_kv(value, module.num_key_value_groups) + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling + if attention_mask is not None: + attn_weights = attn_weights + attention_mask + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) + attn_output = torch.matmul(attn_weights, value_states) + attn_output = attn_output.transpose(1, 2).contiguous() + + return attn_output, attn_weights + + +class ESMFold2SWA3DRoPEAttention(nn.Module): + """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. + + The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention + interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), + with the sliding window expressed as an additive attention mask. The custom + flash-attention path (native bidirectional ``window_size``, plus varlen for + packed inputs) is kept as an opt-in backend, selected when + ``_attn_implementation == "flash_attention_2"``. ``config`` is attached by + the parent ``ESMFold2Model`` after construction; it is ``None`` (→ ``sdpa``) + when the module is used standalone. + """ + + def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: + super().__init__() + self.config = None + self.n_heads = n_heads + self.head_dim = d_model // n_heads + self.scale = self.head_dim**-0.5 + self.half_window = half_window + # No grouped-query attention; identity repeat keeps the interface happy. + self.num_key_value_groups = 1 + # Bidirectional encoder: never let the sdpa/flash interface default to + # causal masking when attention_mask happens to be None. + self.is_causal = False + + self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) + self.out_proj = nn.Linear(d_model, d_model, bias=False) + self.gate_proj = nn.Linear(d_model, d_model, bias=False) + + def forward(self, x: Tensor, attention_params: tuple) -> Tensor: + B, N = x.shape[:2] + cos, sin = attention_params[0], attention_params[1] + + x_input = x + qkv = self.Wqkv(x) + qkv = qkv.view(B, N, 3, self.n_heads, self.head_dim).permute(2, 0, 1, 3, 4) + q, k, v = qkv.unbind(0) + q, k = qk_norm(q), qk_norm(k) + + q = apply_rotary_emb_3d(q, cos, sin) + k = apply_rotary_emb_3d(k, cos, sin) + + input_dtype = q.dtype + if q.dtype not in (torch.float16, torch.bfloat16): + q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() + + attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" + use_flash = attn_impl == "flash_attention_2" and FLASH_ATTN_AVAILABLE + + if use_flash and len(attention_params) > 2: + indices, cu_seqlens, max_seqlen = ( + attention_params[2], + attention_params[3], + attention_params[4], + ) + q_unpad = index_first_axis(q.reshape(-1, self.n_heads, self.head_dim), indices) + k_unpad = index_first_axis(k.reshape(-1, self.n_heads, self.head_dim), indices) + v_unpad = index_first_axis(v.reshape(-1, self.n_heads, self.head_dim), indices) + out_unpad = flash_attn_varlen_func( + q_unpad, + k_unpad, + v_unpad, + cu_seqlens, + cu_seqlens, + max_seqlen, + max_seqlen, + softmax_scale=self.scale, + window_size=(self.half_window, self.half_window), + ) + out = pad_input(out_unpad, indices, B, N) + elif use_flash: + out = flash_attn_func( + q, + k, + v, + softmax_scale=self.scale, + window_size=(self.half_window, self.half_window), + ) + else: + if len(attention_params) > 2: + valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) + valid[attention_params[2]] = True + valid = valid.view(B, N) + else: + valid = torch.ones(B, N, dtype=torch.bool, device=q.device) + rank = torch.cumsum(valid, dim=1) - 1 + within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window + allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) + allowed |= torch.eye(N, dtype=torch.bool, device=q.device) + # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. + attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) + attn_mask = attn_mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(q.dtype).min) + + attention_interface: Callable = eager_attention_forward + if attn_impl != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) + out, _ = attention_interface( + self, + q.transpose(1, 2), + k.transpose(1, 2), + v.transpose(1, 2), + attn_mask, + dropout=0.0, + scaling=self.scale, + ) + out = out * valid.unsqueeze(-1).unsqueeze(-1) + + out = out.to(input_dtype).reshape(B, N, -1) + out = out * torch.sigmoid(self.gate_proj(x_input)) + return self.out_proj(out) + + +# =========================================================================== +# ESMFold2SWAAtomBlock, ESMFold2SWAAtomTransformer +# =========================================================================== + + +def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: + return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift + + +def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: + return x + gate * y + + +class ESMFold2SWAAtomBlock(nn.Module): + """adaLN-Zero + SWA attention + ESMFold2SwiGLU FFN. + + Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight + """ + + def __init__( + self, + d_atom: int, + n_heads: int, + half_window: int = 64, + expansion_ratio: int = 2, + ) -> None: + super().__init__() + # adaln-Zero gate; zero-init lives in ESMFold2PreTrainedModel._init_weights. + self.adaln_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_atom, 6 * d_atom, bias=False)) + + self.attn = ESMFold2SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) + self.ffn = ESMFold2SwiGLUFFN(d_atom, expansion_ratio) + + def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: + mod = self.adaln_modulation(c_l) + if mod.dim() == 2: + mod = mod.unsqueeze(1) + shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) + + attn_input = _rms_adaln(x, scale_a, shift_a) + attn_out = self.attn(attn_input, attention_params) + x = _gated_residual(x, gate_a, attn_out) + + ffn_input = _rms_adaln(x, scale_f, shift_f) + ffn_out = self.ffn(ffn_input) + x = _gated_residual(x, gate_f, ffn_out) + return x + + +class ESMFold2SWAAtomTransformer(nn.Module): + """Stack of SWAAtomBlocks.""" + + def __init__( + self, + d_atom: int = 128, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.swa_window_size = swa_window_size + self.head_dim = d_atom // n_heads + self.spatial_rope_base_frequency = spatial_rope_base_frequency + self.n_spatial_rope_pairs_per_axis = n_spatial_rope_pairs_per_axis + self.n_uid_rope_pairs = n_uid_rope_pairs + self.uid_rope_base_frequency = uid_rope_base_frequency + + self.blocks = nn.ModuleList( + [ + ESMFold2SWAAtomBlock( + d_atom=d_atom, + n_heads=n_heads, + half_window=swa_window_size // 2, + expansion_ratio=expansion_ratio, + ) + for _ in range(n_blocks) + ] + ) + + def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: + return build_3d_rope( + ref_pos=ref_pos, + ref_space_uid=ref_space_uid, + head_dim=self.head_dim, + n_spatial_per_axis=self.n_spatial_rope_pairs_per_axis, + n_uid_pairs=self.n_uid_rope_pairs, + spatial_base_freq=self.spatial_rope_base_frequency, + uid_base_freq=self.uid_rope_base_frequency, + ) + + def forward( + self, + q_l: Tensor, + c_l: Tensor, + attention_params: tuple, + return_intermediates: bool = False, + ) -> Tensor | tuple[Tensor, list[Tensor]]: + intermediates: list[Tensor] = [] + for block in self.blocks: + q_l = block(q_l, c_l, attention_params) + if return_intermediates: + intermediates.append(q_l) + if return_intermediates: + return q_l, intermediates + return q_l + + +# =========================================================================== +# ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) +# =========================================================================== + + +class ESMFold2AtomEncoder(nn.Module): + """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. + + Args: + d_atom: atom hidden dim + d_token: token dim for atom_to_token aggregation + n_blocks, n_heads, swa_window_size, expansion_ratio: transformer params + structure_prediction: if True, creates coords_linear and uses full d_token + spatial_rope_base_frequency, n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs, uid_rope_base_frequency: 3D RoPE config + """ + + def __init__( + self, + d_atom: int = 128, + d_token: int = 768, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + structure_prediction: bool = True, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.d_atom = d_atom + self.d_token = d_token + self.structure_prediction = structure_prediction + + self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) + self.atom_norm = ESMFold2LayerNorm(d_atom) + + if structure_prediction: + self.coords_linear = nn.Linear(6, d_atom, bias=False) + + self.atom_transformer = ESMFold2SWAAtomTransformer( + d_atom=d_atom, + n_blocks=n_blocks, + n_heads=n_heads, + swa_window_size=swa_window_size, + expansion_ratio=expansion_ratio, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # Output aggregation: d_token for structure prediction, d_token//2 for inputs + out_dim = d_token if structure_prediction else d_token // 2 + self.atom_to_token_linear = nn.Linear(d_atom, out_dim, bias=False) + + def forward( + self, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + r_l: Tensor | None = None, + pred_r1: Tensor | None = None, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + inference_cache: dict | None = None, + ) -> tuple[Tensor, Tensor, Tensor, tuple, list[Tensor]]: + """Returns (a, q, c, attention_params, intermediates). + + ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, + attention indices, n_tokens) across diffusion steps. + """ + B, N = ref_pos.shape[:2] + + layer_cache = None + if inference_cache is not None: + layer_cache = inference_cache.setdefault("atomencoder", {}) + + if layer_cache is None or len(layer_cache) == 0: + atom_feats = torch.cat( + [ + ref_pos, + ref_charge.unsqueeze(-1), + atom_attention_mask.unsqueeze(-1), + ref_element, + ref_atom_name_chars.reshape(B, N, MAX_CHARS * CHAR_VOCAB_SIZE), + ], + dim=-1, + ) + c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( + self.atom_linear.weight.dtype + ) + cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) + cos = cos.repeat_interleave(num_diffusion_samples, 0) + sin = sin.repeat_interleave(num_diffusion_samples, 0) + mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) + seqlens = mask_exp.sum(dim=-1, dtype=torch.int32) + indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() + max_seqlen = int(seqlens.max().item()) + cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) + attention_params = (cos, sin, indices, cu_seqlens, max_seqlen) + n_tokens = int(atom_to_token.max().item()) + 1 + if layer_cache is not None: + layer_cache["c_base"] = c_base + layer_cache["attention_params"] = attention_params + layer_cache["mask_exp"] = mask_exp + layer_cache["n_tokens"] = n_tokens + layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + else: + c_base = layer_cache["c_base"] + attention_params = layer_cache["attention_params"] + mask_exp = layer_cache["mask_exp"] + n_tokens = layer_cache["n_tokens"] + + c = c_base + + q = c + + if self.structure_prediction and r_l is not None: + q = q.repeat_interleave(num_diffusion_samples, 0) + if pred_r1 is None: + pred_r1 = torch.zeros_like(r_l) + r_input = torch.cat([r_l, pred_r1], dim=-1) + r_to_q = self.coords_linear(r_input.to(self.coords_linear.weight.dtype)) + q = q + r_to_q + + c = c.repeat_interleave(num_diffusion_samples, 0) + + result = self.atom_transformer( + q_l=q, + c_l=c, + attention_params=attention_params, + return_intermediates=return_intermediates, + ) + if return_intermediates: + q, intermediates = result + else: + q = result + intermediates = [] + + q_to_a = F.relu(self.atom_to_token_linear(q)) + if layer_cache is not None and "atom_to_token_exp" in layer_cache: + atom_to_token_exp = layer_cache["atom_to_token_exp"] + else: + atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) + + return a, q, c, attention_params, intermediates + + +# =========================================================================== +# ESMFold2AtomDecoder +# =========================================================================== + + +class ESMFold2AtomDecoder(nn.Module): + """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear.""" + + def __init__( + self, + d_atom: int = 128, + d_token: int = 768, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) + + self.atom_transformer = ESMFold2SWAAtomTransformer( + d_atom=d_atom, + n_blocks=n_blocks, + n_heads=n_heads, + swa_window_size=swa_window_size, + expansion_ratio=expansion_ratio, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + self.norm = ESMFold2LayerNorm(d_atom) + self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) + + def forward( + self, + a_i: Tensor, + q_l: Tensor, + c_l: Tensor, + p_lm: tuple, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + ) -> tuple[Tensor, list[Tensor]]: + """Returns (r_update, intermediates).""" + atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + a_to_q = self.token_to_atom_linear(a_i) + a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) + q_l = q_l + a_to_q + + result = self.atom_transformer( + q_l=q_l, + c_l=c_l, + attention_params=p_lm, + return_intermediates=return_intermediates, + ) + if return_intermediates: + q_l, intermediates = result + else: + q_l = result + intermediates = [] + + r_l = self.output_linear(self.norm(q_l.float()).to(q_l.dtype)) + return r_l, intermediates + + +# =========================================================================== +# ESMFold2AttentionPairBias (ESMFold2DiffusionTransformer attention block) +# =========================================================================== + + +class ESMFold2AttentionPairBias(nn.Module): + """Gated multi-head attention with pair bias conditioning.""" + + def __init__( + self, + d_model: int, + d_pair: int, + num_heads: int, + d_cond: int | None = None, + use_conditioning: bool = True, + ) -> None: + super().__init__() + self.d_model = d_model + self.num_heads = num_heads + self.head_dim = d_model // num_heads + self.scale = self.head_dim**-0.5 + d_cond = d_cond or d_model + + if use_conditioning: + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. + self.out_gate = nn.Linear(d_cond, d_model, bias=True) + else: + self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) + + self.q_proj = nn.Linear(d_model, d_model, bias=True) + self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) + self.g_proj = nn.Linear(d_model, d_model, bias=False) + self.out_proj = nn.Linear(d_model, d_model, bias=False) + + if d_pair > 0: + self.pair_norm = ESMFold2LayerNorm(d_pair, eps=1e-5) + self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) + + def compute_pair_bias(self, z: Tensor, bsz: int, num_diffusion_samples: int = 1) -> Tensor: + """Project the (normed) pair representation to per-head attention biases. + + Depends only on ``z`` and this block's fixed weights, so it is invariant + across diffusion sampling steps — the sampler computes it once and reuses + it (see ``ESMFold2DiffusionTransformer.forward``). Bit-identical to computing it + inline every step. + """ + if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: + z = z.repeat_interleave(num_diffusion_samples, dim=0) + if z.dim() == 4: + return self.pair_bias_proj(self.pair_norm(z.float()).to(z.dtype)) + return z.unsqueeze(-1) + + def forward( + self, + a: Tensor, + s: Tensor | None, + z: Tensor, + attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + pair_bias: Tensor | None = None, + ) -> Tensor: + bsz, n_queries, d_model = a.shape + + if s is not None: + x = self.adaln(a, s) + else: + x = self.pre_norm(a.float()).to(a.dtype) + + n_keys = x.shape[1] + q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) + kv = self.kv_proj(x) + k, v = kv.chunk(2, dim=-1) + k = k.view(bsz, n_keys, self.num_heads, self.head_dim) + v = v.view(bsz, n_keys, self.num_heads, self.head_dim) + + if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: + attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) + + # Standard attention with pair bias + g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) + + logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale + + # ``pair_bias`` is step-invariant; the diffusion sampler precomputes and + # caches it across steps. Compute inline when not supplied (e.g. uncached). + if pair_bias is None: + pair_bias = self.compute_pair_bias(z, bsz, num_diffusion_samples) + logits = logits + pair_bias.to(dtype=logits.dtype) + + if attention_mask is not None: + min_val = torch.finfo(logits.dtype).min + mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) + logits = logits + mask_bias.to(dtype=logits.dtype) + + attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) + ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) + ctx = g * ctx + out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) + + if s is not None: + out = torch.sigmoid(self.out_gate(s)) * out + return out + + +# =========================================================================== +# ESMFold2ConditionedTransitionBlock +# =========================================================================== + + +class ESMFold2ConditionedTransitionBlock(nn.Module): + """Conditioned ESMFold2SwiGLU transition with adaptive layer norm.""" + + def __init__( + self, + d_model: int, + d_cond: int | None = None, + transition_multiplier: int = 2, + use_conditioning: bool = True, + ) -> None: + super().__init__() + d_cond = d_cond or d_model + hidden = transition_multiplier * d_model + + if use_conditioning: + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. + self.output_gate = nn.Linear(d_cond, d_model, bias=True) + else: + self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) + + self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) + self.lin_out = nn.Linear(hidden, d_model, bias=False) + + def forward(self, a: Tensor, s: Tensor | None) -> Tensor: + if s is not None: + x = self.adaln(a, s) + else: + x = self.pre_norm(a.float()).to(a.dtype) + + swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) + b = F.silu(swish_a) * swish_b + out = self.lin_out(b) + + if s is not None: + out = torch.sigmoid(self.output_gate(s)) * out + return out + + +# =========================================================================== +# ESMFold2DiffusionTransformer (token transformer) +# =========================================================================== + + +class ESMFold2DiffusionTransformer(nn.Module): + """Diffusion denoising transformer with attention pair bias.""" + + def __init__( + self, + d_model: int, + d_pair: int, + num_heads: int, + num_blocks: int, + d_cond: int | None = None, + transition_multiplier: int = 2, + use_conditioning: bool = True, + ) -> None: + super().__init__() + d_cond = d_cond or d_model + + self.attn_blocks = nn.ModuleList( + [ + ESMFold2AttentionPairBias( + d_model=d_model, + d_pair=d_pair, + num_heads=num_heads, + d_cond=d_cond, + use_conditioning=use_conditioning, + ) + for _ in range(num_blocks) + ] + ) + self.transition_blocks = nn.ModuleList( + [ + ESMFold2ConditionedTransitionBlock( + d_model=d_model, + d_cond=d_cond, + transition_multiplier=transition_multiplier, + use_conditioning=use_conditioning, + ) + for _ in range(num_blocks) + ] + ) + + def forward( + self, + a: Tensor, + s: Tensor | None, + z: Tensor, + attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + inference_cache: dict | None = None, + ) -> tuple[Tensor, list[Tensor]]: + intermediates: list[Tensor] = [] + x = a + bsz = a.shape[0] + # Each block's pair bias depends only on the (step-invariant) conditioning + # pair ``z`` and fixed weights, so compute it once per block and reuse it + # across every diffusion sampling step. Bit-identical to recomputing it + # each step; the cache lives in the sampler's per-fold ``inference_cache``. + bias_cache = None if inference_cache is None else inference_cache.setdefault("token_pair_bias", {}) + for i, (attn, transition) in enumerate(zip(self.attn_blocks, self.transition_blocks)): + if bias_cache is None: + pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) + elif i in bias_cache: + pair_bias = bias_cache[i] + else: + pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) + bias_cache[i] = pair_bias + x = x + attn( + x, + s, + z, + attention_mask=attention_mask, + num_diffusion_samples=num_diffusion_samples, + pair_bias=pair_bias, + ) + x = x + transition(x, s) + if return_intermediates: + intermediates.append(x) + return x, intermediates + + +# =========================================================================== +# ESMFold2DiffusionConditioning +# =========================================================================== + + +class ESMFold2DiffusionConditioning(nn.Module): + """Conditions pair and single representations on noise timestep.""" + + def __init__( + self, + c_z: int = 256, + c_s: int = 768, + c_s_inputs: int = 451, + sigma_data: float = 16.0, + fourier_dim: int = 256, + transition_multiplier: int = 2, + layer_norm_eps: float = 1e-5, + ) -> None: + super().__init__() + self.sigma_data = float(sigma_data) + self.c_z = c_z + self.c_s = c_s + self.c_s_inputs = c_s_inputs + + self.z_input_norm = ESMFold2LayerNorm(2 * c_z, eps=layer_norm_eps) + self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) + self.z_transitions = nn.ModuleList( + [ESMFold2TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] + ) + + self.s_input_norm = ESMFold2LayerNorm(c_s_inputs, eps=layer_norm_eps) + self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) + self.fourier = ESMFold2FourierEmbedding(fourier_dim) + self.noise_norm = ESMFold2LayerNorm(fourier_dim, eps=layer_norm_eps) + self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) + self.s_transitions = nn.ModuleList( + [ESMFold2TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] + ) + + def forward( + self, + t_hat: Tensor, + s_inputs: Tensor, + z_trunk: Tensor, + relative_position_encoding: Tensor, + sigma_data: float | None = None, + num_diffusion_samples: int = 1, + inference_cache: dict[str, Tensor] | None = None, + ) -> tuple[Tensor, Tensor]: + sigma = self.sigma_data if sigma_data is None else float(sigma_data) + base_batch = z_trunk.shape[0] + target_batch = base_batch * num_diffusion_samples + + # z conditioning (cached across diffusion steps — independent of t_hat) + if inference_cache is not None and "z" in inference_cache: + z = inference_cache["z"] + else: + z_rel = relative_position_encoding.to(dtype=torch.float32) + z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) + # The relpos/coords conditioning is fp32; z_input_norm keeps it fp32, + # then we hand off to z_proj in the model's compute dtype. + z = self.z_proj(self.z_input_norm(z).to(self.z_proj.weight.dtype)) + for block in self.z_transitions: + z = z + block(z) + if inference_cache is not None: + inference_cache["z"] = z + + # s conditioning + s_inputs_eff = s_inputs + if s_inputs_eff.shape[0] != target_batch: + s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) + + s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype)) + + # Noise embedding + t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) + if t.numel() == 1: + t = t.expand(target_batch) + elif t.shape[0] != target_batch: + t = t.repeat_interleave(num_diffusion_samples, 0) + t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) + n = self.fourier(t_noise) + n = self.noise_proj(self.noise_norm(n.float()).to(self.noise_proj.weight.dtype)) + s = s + n.unsqueeze(1) + + for block in self.s_transitions: + s = s + block(s) + + return s, z + + +# =========================================================================== +# ESMFold2DiffusionModule +# =========================================================================== + + +class ESMFold2DiffusionModule(nn.Module): + """Diffusion denoising module for structure prediction.""" + + def __init__( + self, + c_atom: int = 128, + c_token: int = 768, + c_z: int = 256, + c_s_inputs: int = 451, + sigma_data: float = 16.0, + fourier_dim: int = 256, + atom_num_blocks: int = 3, + atom_num_heads: int = 4, + token_num_blocks: int = 12, + token_num_heads: int = 16, + transition_multiplier: int = 2, + swa_window_size: int = 128, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.sigma_data = float(sigma_data) + + self.conditioning = ESMFold2DiffusionConditioning( + c_z=c_z, + c_s=c_token, # conditioning s output is c_token + c_s_inputs=c_s_inputs, + sigma_data=sigma_data, + fourier_dim=fourier_dim, + transition_multiplier=transition_multiplier, + ) + + # Atom encoder (structure_prediction=True, with coords_linear) + self.atom_encoder = ESMFold2AtomEncoder( + d_atom=c_atom, + d_token=c_token, + n_blocks=atom_num_blocks, + n_heads=atom_num_heads, + swa_window_size=swa_window_size, + expansion_ratio=2, + structure_prediction=True, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # Atom decoder + self.atom_decoder = ESMFold2AtomDecoder( + d_atom=c_atom, + d_token=c_token, + n_blocks=atom_num_blocks, + n_heads=atom_num_heads, + swa_window_size=swa_window_size, + expansion_ratio=2, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # zero-init lives in ESMFold2PreTrainedModel._init_weights. + self.s_to_token = nn.Linear(c_token, c_token, bias=False) + + # Token transformer (ESMFold2DiffusionTransformer with pair bias) + self.token_transformer = ESMFold2DiffusionTransformer( + d_model=c_token, + d_pair=c_z, + num_heads=token_num_heads, + num_blocks=token_num_blocks, + d_cond=c_token, + transition_multiplier=transition_multiplier, + use_conditioning=True, + ) + + self.s_step_norm = ESMFold2LayerNorm(c_token) + self.token_norm = ESMFold2LayerNorm(c_token) + + def forward( + self, + x_noisy: Tensor, + t_hat: Tensor, + ref_pos: Tensor, + ref_charge: Tensor, + ref_mask: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + tok_idx: Tensor, + s_inputs: Tensor, + z_trunk: Tensor, + relative_position_encoding: Tensor, + asym_id: Tensor, + residue_index: Tensor, + entity_id: Tensor, + token_index: Tensor, + sym_id: Tensor, + sigma_data: float | None = None, + token_attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + return_atom_repr: bool = False, + inference_cache: dict[str, Tensor] | None = None, + ) -> dict[str, Tensor | None]: + bsz = x_noisy.shape[0] + sigma = self.sigma_data if sigma_data is None else float(sigma_data) + t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape(-1) + if t.numel() == 1: + t = t.expand(bsz) + + # Step 1: conditioning (pair z is cached across diffusion steps) + s, z = self.conditioning( + t_hat=t, + s_inputs=s_inputs, + z_trunk=z_trunk, + relative_position_encoding=relative_position_encoding, + sigma_data=sigma, + num_diffusion_samples=num_diffusion_samples, + inference_cache=inference_cache, + ) + + # Step 2: normalize noisy coords + denom = torch.sqrt(t * t + sigma * sigma) + r_noisy = x_noisy / denom[:, None, None] + + # Step 3: atom encoder + a, q_skip, c_skip, p_skip, enc_intermediates = self.atom_encoder( + ref_pos=ref_pos, + atom_attention_mask=ref_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=tok_idx, + r_l=r_noisy, + num_diffusion_samples=num_diffusion_samples, + return_intermediates=return_atom_repr, + inference_cache=inference_cache, + ) + + # Step 4: add conditioned s + a = a + self.s_to_token(self.s_step_norm(s.float()).to(s.dtype)) + + # Step 5: token transformer (pair bias is cached across steps via inference_cache) + a, _ = self.token_transformer( + a, + s, + z, + attention_mask=token_attention_mask, + num_diffusion_samples=num_diffusion_samples, + inference_cache=inference_cache, + ) + + # Step 6: token norm + a = self.token_norm(a.float()).to(a.dtype) + + # Step 7: atom decoder + r_update, dec_intermediates = self.atom_decoder( + a_i=a, + q_l=q_skip, + c_l=c_skip, + p_lm=p_skip, + atom_to_token=tok_idx, + atom_attention_mask=ref_mask, + num_diffusion_samples=num_diffusion_samples, + return_intermediates=return_atom_repr, + ) + + # Step 8: compute denoised output + sigma2 = sigma * sigma + t2 = t * t + out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy + out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update + + # Collect atom intermediates from encoder + decoder + atom_intermediates: Tensor | None = None + if return_atom_repr: + all_ints = enc_intermediates + dec_intermediates + if all_ints: + atom_intermediates = torch.stack(all_ints, dim=2) + + return { + "x_denoised": out, + "atom_intermediates": atom_intermediates, + } + + +# =========================================================================== +# ESMFold2DiffusionStructureHead +# =========================================================================== + + +class ESMFold2DiffusionStructureHead(nn.Module): + """Wrapper around ESMFold2DiffusionModule with diffusion sampling.""" + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + dm = config.structure_head.diffusion_module + swa_cfg = config.inputs.atom_encoder + sh = config.structure_head + + self.diffusion_module = ESMFold2DiffusionModule( + c_atom=dm.c_atom, + c_token=dm.c_token, + c_z=dm.c_z, + c_s_inputs=dm.c_s_inputs, + sigma_data=dm.sigma_data, + fourier_dim=dm.fourier_dim, + atom_num_blocks=dm.atom_num_blocks, + atom_num_heads=dm.atom_num_heads, + token_num_blocks=dm.token_num_blocks, + token_num_heads=dm.token_num_heads, + transition_multiplier=dm.transition_multiplier, + swa_window_size=swa_cfg.swa_window_size, + spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, + uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, + ) -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch import Tensor + # Sampling hyperparameters + self.sigma_data = dm.sigma_data + self.gamma_0 = sh.gamma_0 + self.gamma_min = sh.gamma_min + self.noise_scale = sh.noise_scale + self.step_scale = sh.step_scale + self.inference_s_max = sh.inference_s_max + self.inference_s_min = sh.inference_s_min + self.inference_p = sh.inference_p + self.inference_num_steps = sh.inference_num_steps + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def inference_noise_schedule(self, num_steps: int | None = None, device: torch.device | None = None) -> Tensor: + """Karras power-law noise schedule.""" + steps = self.inference_num_steps if num_steps is None else int(num_steps) + if steps == 1: + return torch.tensor( + [self.inference_s_max * self.sigma_data, 0.0], + device=device, + dtype=torch.float32, + ) + p = float(self.inference_p) + inv_p = 1.0 / p + k = torch.arange(steps, device=device, dtype=torch.float32) + base = self.inference_s_max**inv_p + (k / (steps - 1)) * ( + self.inference_s_min**inv_p - self.inference_s_max**inv_p + ) + schedule = self.sigma_data * base.pow(p) + return F.pad(schedule, (0, 1), value=0.0) -from ...modeling_utils import PreTrainedModel -from .configuration_esmfold2 import ESMFold2Config -from .modeling_esmfold2_common import ( - CHAR_VOCAB_SIZE, - MAX_ATOMIC_NUMBER, - NUM_RES_TYPES, - DiffusionStructureHead, - FoldingTrunk, - InputsEmbedder, - LanguageModelShim, - MSAPairWeightedAveraging, - OuterProductMean, - ResIdxAsymIdSymIdEntityIdEncoding, - RowAttentionPooling, - SWA3DRoPEAttention, - Transition, - TriangleMultiplicativeUpdate, - _categorical_mean, - _compute_intra_token_idx, - compute_lm_hidden_states, - gather_rep_atom_coords, - gather_token_to_atom, -) - - -_EPS = 1e-6 + @staticmethod + def _random_rotations(n: int, dtype: torch.dtype, device: torch.device) -> Tensor: + q = torch.randn((n, 4), dtype=dtype, device=device) + scale = torch.sqrt((q * q).sum(dim=1)) + signs = torch.where(q[:, 0] < 0, -scale, scale) + q = q / signs[:, None] + r, i, j, k = torch.unbind(q, dim=-1) + two_s = 2.0 / (q * q).sum(dim=-1) + return torch.stack( + ( + 1 - two_s * (j * j + k * k), + two_s * (i * j - k * r), + two_s * (i * k + j * r), + two_s * (i * j + k * r), + 1 - two_s * (i * i + k * k), + two_s * (j * k - i * r), + two_s * (i * k - j * r), + two_s * (j * k + i * r), + 1 - two_s * (i * i + j * j), + ), + dim=-1, + ).reshape(n, 3, 3) + + def _center_random_augmentation( + self, x: Tensor, atom_mask: Tensor, second_coords: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + """Algorithm 19: center + random rotation + translation.""" + bsz = x.shape[0] + mask = atom_mask.unsqueeze(-1) # [B, A, 1] + denom = mask.sum(dim=1, keepdim=True).clamp(min=1) + mean = (x * mask).sum(dim=1, keepdim=True) / denom + x = x - mean + if second_coords is not None: + second_coords = second_coords - mean + + r = self._random_rotations(bsz, x.dtype, x.device) + x = torch.einsum("bmd,bds->bms", x, r) + if second_coords is not None: + second_coords = torch.einsum("bmd,bds->bms", second_coords, r) + + t = torch.randn_like(x[:, 0:1, :]) + x = x + t + if second_coords is not None: + second_coords = second_coords + t + return x, second_coords + + @staticmethod + def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> Tensor: + """Kabsch alignment: align x to x_gt with weights w.""" + w = (mask * w).unsqueeze(-1) # [B, N, 1] + denom = w.sum(dim=-2, keepdim=True).clamp(min=1e-8) + mu = (x * w).sum(dim=-2, keepdim=True) / denom + mu_gt = (x_gt * w).sum(dim=-2, keepdim=True) / denom + x_c = x - mu + xgt_c = x_gt - mu_gt + H = torch.einsum("bni,bnj->bij", w * xgt_c, x_c) + H32 = H.float() + U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) + det = torch.linalg.det(U @ Vh) + ones = torch.ones_like(det) + R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to(H.dtype) + return x_c @ R.transpose(-1, -2) + mu_gt + + # ------------------------------------------------------------------ + # Sampling + # ------------------------------------------------------------------ + + @torch.inference_mode() + def sample( + self, + z_trunk: Tensor, + s_inputs: Tensor, + relative_position_encoding: Tensor, + ref_pos: Tensor, + ref_charge: Tensor, + ref_mask: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + tok_idx: Tensor, + asym_id: Tensor, + residue_index: Tensor, + entity_id: Tensor, + token_index: Tensor, + sym_id: Tensor, + token_attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + num_sampling_steps: int | None = None, + max_inference_sigma: float | None = 256.0, + noise_scale: float | None = None, + step_scale: float | None = None, + return_atom_repr: bool = False, + use_inference_cache: bool = True, + denoising_early_exit_rmsd: float | None = None, + ) -> dict[str, Tensor | None]: + """Diffusion sampling (Algorithm 18). + + ``num_sampling_steps`` is the number of denoising steps actually run. + When ``max_inference_sigma`` is set, the Karras schedule built with + ``num_sampling_steps`` entries would lose its high-σ tail to the cap, + so we inflate the underlying schedule length here to land back at the + requested step count post-truncation. + """ + n_atoms = tok_idx.shape[1] + device = s_inputs.device + target_batch = s_inputs.shape[0] * num_diffusion_samples + + inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None + + steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) + + schedule = self.inference_noise_schedule(steps, device) + if max_inference_sigma is not None: + schedule = schedule[schedule <= float(max_inference_sigma)] + schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) + + lam = self.noise_scale if noise_scale is None else float(noise_scale) + eta = self.step_scale if step_scale is None else float(step_scale) + + x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) + atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() + + gammas = torch.where( + schedule > self.gamma_min, + torch.full_like(schedule, self.gamma_0), + torch.zeros_like(schedule), + ) + + x_denoised_prev: Tensor | None = None + diff_atom_intermediates: Tensor | None = None + + step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) + num_steps = len(step_pairs) + + for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): + x, x_denoised_prev = self._center_random_augmentation(x, atom_mask, second_coords=x_denoised_prev) + + sigma_tm_val = float(sigma_tm.item()) + t_hat_val = sigma_tm_val * (1.0 + float(gamma.item())) + eps_std = lam * max(t_hat_val**2 - sigma_tm_val**2, 0.0) ** 0.5 + x_noisy = x + eps_std * torch.randn_like(x) + + is_last_step = step_idx == num_steps - 1 + request_atom_repr = return_atom_repr and (is_last_step or denoising_early_exit_rmsd is not None) + + dm_out = self.diffusion_module( + x_noisy=x_noisy, + t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), + ref_pos=ref_pos, + ref_charge=ref_charge, + ref_mask=ref_mask, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + ref_space_uid=ref_space_uid, + tok_idx=tok_idx, + s_inputs=s_inputs, + z_trunk=z_trunk, + relative_position_encoding=relative_position_encoding, + asym_id=asym_id, + residue_index=residue_index, + entity_id=entity_id, + token_index=token_index, + sym_id=sym_id, + token_attention_mask=token_attention_mask, + num_diffusion_samples=num_diffusion_samples, + return_atom_repr=request_atom_repr, + inference_cache=inference_cache, + ) + + x_denoised = dm_out["x_denoised"] + if request_atom_repr: + diff_atom_intermediates = dm_out.get("atom_intermediates") + + # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts + # to fp32 internally for the SVD/det. + x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) + x_noisy = x_noisy.to(dtype=x_denoised.dtype) + + # ODE/SDE step + sigma_t_val = float(sigma_t.item()) + denoised_over_sigma = (x_noisy - x_denoised) / t_hat_val + x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma + + # Denoising early-exit: stop when consecutive predictions converge + if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: + aligned = self._weighted_rigid_align( + x_denoised_prev.float(), + x_denoised.float(), + atom_mask, + atom_mask, + ) + diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) + per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() + if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: + x = x_denoised + x_denoised_prev = x_denoised + break + + x_denoised_prev = x_denoised + + result: dict[str, Tensor | None] = { + "sample_atom_coords": x, + } + if return_atom_repr: + result["diff_atom_intermediates"] = diff_atom_intermediates + return result + + +# =========================================================================== +# ESMFold2RowAttentionPooling +# =========================================================================== + + +class ESMFold2RowAttentionPooling(nn.Module): + """Row-wise attention pooling: attn_proj, out_proj.""" + + def __init__(self, d_pair: int, d_single: int) -> None: + super().__init__() + self.attn_proj = nn.Linear(d_pair, 1, bias=False) + self.out_proj = nn.Linear(d_pair, d_single, bias=False) + + def forward(self, z: Tensor, mask: Tensor) -> Tensor: + scores = self.attn_proj(z).squeeze(-1) + mask_bias = torch.where( + mask[:, None, :].bool(), + torch.zeros_like(scores), + torch.full_like(scores, -1e9), + ) + scores = scores + mask_bias + weights = F.softmax(scores, dim=-1, dtype=torch.float32).to(scores.dtype) + pooled = torch.einsum("bnm,bnmd->bnd", weights, z) + return self.out_proj(pooled) + + +# =========================================================================== +# ESMFold2InputsEmbedder +# =========================================================================== + + +class ESMFold2InputsEmbedder(nn.Module): + """Embeds input features including atom-level encoding via SWA attention.""" + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + swa_cfg = config.inputs.atom_encoder + + self.atom_attention_encoder = ESMFold2AtomEncoder( + d_atom=swa_cfg.d_atom, + d_token=swa_cfg.d_token, + n_blocks=swa_cfg.n_blocks, + n_heads=swa_cfg.n_heads, + swa_window_size=swa_cfg.swa_window_size, + expansion_ratio=swa_cfg.expansion_ratio, + structure_prediction=False, # no coords_linear + spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, + uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, + ) + + def forward( + self, + aatype: Tensor, + profile: Tensor, + deletion_mean: Tensor, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + ) -> Tensor: + """Embed inputs into per-token features. + + Returns: + [B, L, d_inputs] concatenation of atom encoding, aatype, profile, + and deletion_mean. + """ + a, _q, _c, _attn_params, _intermediates = self.atom_attention_encoder( + ref_pos=ref_pos, + atom_attention_mask=atom_attention_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=atom_to_token, + ) + # The continuous input features are fp32; fold them into the atom + # encoding's (compute) dtype so the single representation is one dtype. + dtype = a.dtype + return torch.cat( + [a, aatype.to(dtype), profile.to(dtype), deletion_mean.unsqueeze(-1).to(dtype)], + dim=-1, + ) + + +# =========================================================================== +# ESMFold2ResIdxAsymIdSymIdEntityIdEncoding (trunk relative position) +# =========================================================================== + + +class ESMFold2ResIdxAsymIdSymIdEntityIdEncoding(nn.Module): + """embed.weight [d_pair, n_features] where n_features = 2*(2*r_bins+2) + 1 + (2*c_bins+2). + + For default r_bins=32, c_bins=2: 2*66 + 1 + 6 = 139. + """ + + def __init__( + self, + n_relative_residx_bins: int = 32, + n_relative_chain_bins: int = 2, + d_pair: int = 256, + ) -> None: + super().__init__() + self.n_relative_residx_bins = n_relative_residx_bins + self.n_relative_chain_bins = n_relative_chain_bins + self.d_pair = d_pair + + n_feats_residue = 2 * n_relative_residx_bins + 2 + n_feats_token = 2 * n_relative_residx_bins + 2 + n_feats_chain = 2 * n_relative_chain_bins + 2 + n_feats_same_entity = 1 + total_feats = n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity + self.embed = nn.Linear(total_feats, d_pair, bias=False) + + def forward( + self, + residue_index: Tensor, + asym_id: Tensor, + sym_id: Tensor, + entity_id: Tensor, + token_index: Tensor, + ) -> Tensor: + bij_same_chain = asym_id.unsqueeze(2) == asym_id.unsqueeze(1) + bij_same_residue = residue_index.unsqueeze(2) == residue_index.unsqueeze(1) + bij_same_entity = entity_id.unsqueeze(2) == entity_id.unsqueeze(1) + + dij_residue = residue_index.unsqueeze(2) - residue_index.unsqueeze(1) + dij_residue = torch.clip( + dij_residue + self.n_relative_residx_bins, + 0, + 2 * self.n_relative_residx_bins, + ) + dij_residue = torch.where(bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1) + aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) + + dij_token = torch.clip( + token_index.unsqueeze(2) - token_index.unsqueeze(1) + self.n_relative_residx_bins, + 0, + 2 * self.n_relative_residx_bins, + ) + dij_token = torch.where( + bij_same_chain & bij_same_residue, + dij_token, + 2 * self.n_relative_residx_bins + 1, + ) + aij_rel_token = F.one_hot(dij_token, 2 * self.n_relative_residx_bins + 2) + + dij_chain = torch.clip( + sym_id.unsqueeze(2) - sym_id.unsqueeze(1) + self.n_relative_chain_bins, + 0, + 2 * self.n_relative_chain_bins, + ) + dij_chain = torch.where(bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain) + aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) + + feats = torch.cat( + [ + aij_rel_pos.float(), + aij_rel_token.float(), + bij_same_entity.float().unsqueeze(-1), + aij_rel_chain.float(), + ], + dim=-1, + ) + + return self.embed(feats.to(self.embed.weight.dtype)) + + +# =========================================================================== +# ESMFold2SingleToPair (for ESMFold2LanguageModelShim) +# =========================================================================== + + +class ESMFold2SingleToPair(nn.Module): + """downproject, output_mlp (Sequential of Linear, GELU, Linear).""" + + def __init__(self, input_dim: int, downproject_dim: int, output_dim: int) -> None: + super().__init__() + self.downproject = nn.Linear(input_dim, downproject_dim) + self.output_mlp = nn.Sequential( + nn.Linear(2 * downproject_dim, output_dim), + nn.GELU(), + nn.Linear(output_dim, output_dim), + ) + + def forward(self, x: Tensor) -> Tensor: + x = self.downproject(x) + x = torch.cat( + [(x.unsqueeze(2) * x.unsqueeze(1)), (x.unsqueeze(2) - x.unsqueeze(1))], + dim=3, + ) + return self.output_mlp(x) + + +# =========================================================================== +# ESMFold2LanguageModelShim +# =========================================================================== + + +class ESMFold2LanguageModelShim(nn.Module): + """Shim holding the trainable projection weights for LM integration. + + Contains: + - base_z_combine: nn.Parameter [num_layers+1] + - base_z_linear: Sequential(ESMFold2LayerNorm(d_model), Linear(d_model, d_z, bias=False)) + - base_z_mlp: Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + """ + + def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: + super().__init__() + + self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + self.base_z_linear = nn.Sequential( + ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False) + ) + self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) + + def forward(self, hidden_states: Tensor) -> Tensor: + """Project pre-computed ESMC hidden states to pair representation. + + Args: + hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. + + Returns: + [B, L, L, d_pair] pair representation. + """ + # The ESMC backbone may be loaded at a different precision than the trunk + # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. + hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) + # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. + normed = self.base_z_linear[0](hidden_states.float()).to(hidden_states.dtype) + lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] + weights = self.base_z_combine.softmax(0) # [81] + lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] + # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. + pair = self.base_z_mlp[0](lm_z) + lm_z = self.base_z_mlp[1](pair.float()).to(pair.dtype) # [B, L, L, d_z] + return lm_z + + +# =========================================================================== +# ESMFold2 — language-model backbone helpers +# =========================================================================== + + +def compute_lm_hidden_states( + esmc: nn.Module, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + token_mask: Tensor, + pad_to_multiple: int | None = None, +) -> Tensor: + """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. + + Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple + structure tokens but share a single ``(asym_id, residue_index)`` key — + collapse them to one LM token per residue before running the LM (the LM + was trained on per-residue inputs, not per-atom), then scatter the + hidden states back to the per-token layout. + """ + B, L = input_ids.shape + device = input_ids.device + protein_mask = (mol_type == 0) & token_mask + + lm_input_list = [] + lm_lengths = [] + # Per-batch maps from (original protein-token index) to (LM input position). + expand_maps: list[Tensor] = [] + for b in range(B): + mask_b = protein_mask[b] + ids_b = input_ids[b][mask_b] + asym_b = asym_id[b][mask_b] + res_b = residue_index[b][mask_b] + + # Collapse: keep first token per (asym_id, residue_index) key, in + # input order. ``inverse`` maps each original protein-token to its + # collapsed residue index. + keys = torch.stack((asym_b, res_b), dim=1) + unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) + n_unique = unique_keys.size(0) + token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) + first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) + first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) + ordered = torch.argsort(first_pos) + first_pos_ordered = first_pos[ordered] + ids_collapsed = ids_b[first_pos_ordered] + asym_collapsed = asym_b[first_pos_ordered] + remap = torch.empty_like(ordered) + remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) + inverse_ordered = remap[inverse] + + chain_ids = asym_collapsed.unique(sorted=True) + # [BOS] chain1 [EOS BOS] chain2 ... [EOS] + parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] + # Per-chain LM positions accumulate; track them for the expand map. + per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) + cursor = 1 # position 0 is the leading BOS + for i, cid in enumerate(chain_ids): + in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] + parts.append(ids_collapsed[in_chain]) + per_token_lm_pos[in_chain] = torch.arange( + cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long + ) + cursor += in_chain.shape[0] + if i < len(chain_ids) - 1: + parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) + cursor += 2 # EOS + BOS + parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) + lm_seq = torch.cat(parts) + lm_input_list.append(lm_seq) + lm_lengths.append(lm_seq.shape[0]) + + # Original protein-token position → LM input position. + prot_pos_b = mask_b.nonzero(as_tuple=True)[0] + expand_map = torch.full((L,), -1, device=device, dtype=torch.long) + expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] + expand_maps.append(expand_map) + + # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. + max_len = max(lm_lengths) + if pad_to_multiple is not None and pad_to_multiple > 1: + max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple + lm_input_ids = torch.full( + (B, max_len), + 1, + device=device, + dtype=input_ids.dtype, # PAD=1 + ) + for b in range(B): + lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] + + # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). + sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 + sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 + + with torch.inference_mode(): + esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) + + hs = esmc_out.hidden_states # [n_layers+1, B, max_len, D] + n_layers_plus_1, _, _, D = hs.shape + result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) + for b in range(B): + mb = protein_mask[b] + em = expand_maps[b][mb] # [n_protein_tokens] LM positions + # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] + gathered = hs[:, b, em, :].permute(1, 0, 2) + result[b, mb.nonzero(as_tuple=True)[0]] = gathered + + return result.detach() + + +# =========================================================================== +# ESMFold2TriangleMultiplicativeUpdate +# =========================================================================== +@use_kernel_forward_from_hub("ESMFold2TriangleMultiplication") +class ESMFold2TriangleMultiplicativeBlock(nn.Module): + """Triangle multiplicative update block with gated signal routing. + + The O(N^3) triangular contraction below is the trunk's dominant cost. Loading + with ``ESMFold2Model.from_pretrained(..., device_map="cuda", use_kernels=True)`` + (CUDA + inference) swaps the whole block forward for a fused Triton kernel from + the Hub (see the ``hub_kernels`` mapping); the pure-PyTorch ``forward`` here stays + as the reference/fallback. The kernel reads this module's parameters + (``norm_start``/``norm_mix``/``proj_bundle``/``proj_emit``/``proj_gate``) and + matches ``forward``'s ``(pair_grid, visibility)`` signature, returning the + residual-free delta. + """ + + _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} + _VALID_FLOWS = ("outgoing", "incoming") + + def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None: + super().__init__() + if flow not in self._FLOW_TO_EINSUM: + raise ValueError(f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}.") + + self.input_channels = input_channels + self.latent_channels = latent_channels + self.flow = flow + self._einsum_equation = self._FLOW_TO_EINSUM[flow] + self.norm_start = ESMFold2LayerNorm(self.input_channels, eps=_EPS) + self.norm_mix = ESMFold2LayerNorm(self.latent_channels, eps=_EPS) + self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) + self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) + self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) + + # Default chunked for memory on long sequences; tests override with + # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 + # parity checks. + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: + return torch.einsum(self._einsum_equation, left_stream, right_stream) + + def _triangular_contract_chunked(self, left_stream: Tensor, right_stream: Tensor, chunk_size: int) -> Tensor: + """Compute the triangular einsum in chunks along the output i-dimension.""" + L = left_stream.shape[1] if self.flow == "outgoing" else left_stream.shape[2] + chunks = [] + for start in range(0, L, chunk_size): + end = min(start + chunk_size, L) + if self.flow == "outgoing": + chunk = torch.einsum(self._einsum_equation, left_stream[:, start:end], right_stream) + else: + chunk = torch.einsum(self._einsum_equation, left_stream[:, :, start:end], right_stream) + chunks.append(chunk) + return torch.cat(chunks, dim=1) + + def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor: + if visibility is None: + visibility = pair_grid.new_ones(pair_grid.shape[:-1]) + + normalized_grid = self.norm_start(pair_grid.float()).to(pair_grid.dtype) + bundled = self.proj_bundle(normalized_grid) + signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) + # Gates and the O(N^3) contraction run in the activation dtype (bf16). This + # matches the reference: under its autocast the einsum is downcast to bf16, + # and the fused Triton kernel likewise contracts in bf16 — the dtype the + # checkpoint was trained with. Keeping these in fp32 was a (marginal) + # precision *up* that diverges from training and is slower on the trunk's + # dominant op. ``norm_start``/``norm_mix`` stay fp32. A no-op in fp32. + routed = signal * torch.sigmoid(gate_logits) + routed = routed * visibility.unsqueeze(-1) + + left_stream, right_stream = routed.chunk(2, dim=-1) + if self._chunk_size is not None: + contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) + else: + contracted = self._triangular_contract(left_stream, right_stream) + mixed = self.proj_emit(self.norm_mix(contracted.float()).to(self.proj_emit.weight.dtype)) + output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) + return mixed * output_gate + + +class ESMFold2TriangleMultiplicativeUpdate(nn.Module): + """Thin wrapper exposing the triangular mixer with explicit orientation (v3).""" + + def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: + super().__init__() + flow = "outgoing" if _outgoing else "incoming" + self._engine = ESMFold2TriangleMultiplicativeBlock(input_channels=dim, latent_channels=dim, flow=flow) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._engine.set_chunk_size(chunk_size) + + def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: + return self._engine(z, visibility=mask) + + +# =========================================================================== +# ESMFold2FoldingTrunk: ESMFold2Transition, ESMFold2PairUpdateBlock, ESMFold2FoldingTrunk +# =========================================================================== + + +class ESMFold2Transition(nn.Module): + """LayerNorm + ESMFold2SwiGLU feed-forward residual block, chunked along the token axis.""" + + def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: + super().__init__() + self.norm = ESMFold2LayerNorm(d_model) + self.ffn = ESMFold2SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) + # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def forward(self, x: Tensor) -> Tensor: + if self._chunk_size is None or x.shape[1] <= self._chunk_size: + return x + self.ffn(self.norm(x.float()).to(x.dtype)) + out_list: list[Tensor] = [] + for s in range(0, x.shape[1], self._chunk_size): + e = min(s + self._chunk_size, x.shape[1]) + sl = x[:, s:e] + out_list.append(sl + self.ffn(self.norm(sl.float()).to(sl.dtype))) + return torch.cat(out_list, dim=1) + + +class ESMFold2PairUpdateBlock(nn.Module): + """tri_mul_out, tri_mul_in, pair_transition.""" + + def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: + super().__init__() + self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=expansion_ratio) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.tri_mul_out.set_chunk_size(chunk_size) + self.tri_mul_in.set_chunk_size(chunk_size) + self.pair_transition.set_chunk_size(chunk_size) + + def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: + # HF model is inference-only, so the trained row-shared dropout (r=0) is a no-op. + pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) + pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) + pair = self.pair_transition(pair) + return pair + + +class ESMFold2FoldingTrunk(nn.Module): + """ModuleList of PairUpdateBlocks.""" + + def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4) -> None: + super().__init__() + self.blocks = nn.ModuleList( + [ESMFold2PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) for _ in range(n_layers)] + ) + + def set_chunk_size(self, chunk_size: int | None) -> None: + for block in self.blocks: + block.set_chunk_size(chunk_size) + + def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: + for block in self.blocks: + fn = partial(block, pair_attention_mask=pair_attention_mask) + if torch.is_grad_enabled(): + pair = checkpoint(fn, pair, use_reentrant=False) + else: + pair = fn(pair) + return pair + + +# =========================================================================== +# MSA Encoder +# =========================================================================== + + +class ESMFold2OuterProductMean(nn.Module): + """Outer-product mean: maps an MSA representation into a pair update. + + The order of the ``/ n_valid`` divide vs. the ``Wout`` projection is + selectable via ``divide_outer_before_proj`` because different ESMFold2 + checkpoints were trained with different orderings: + + * ``False`` (default): ``Wout(outer) / n_valid`` — the projection bias + is scaled by 1/n_valid alongside the outer product. + * ``True``: ``Wout(outer / n_valid)`` — the projection bias is added + unscaled, post-divide. + """ + + def __init__( + self, + d_msa: int, + d_hidden: int, + d_pair: int, + divide_outer_before_proj: bool = False, + ) -> None: + super().__init__() + self.d_hidden = d_hidden + self.divide_outer_before_proj = divide_outer_before_proj + self.norm = ESMFold2LayerNorm(d_msa) + self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) + self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) + # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. + self._chunk_size: int | None = None + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: + m_norm = self.norm(m.float()).to(m.dtype) + x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) + a, b = x.chunk(2, dim=-1) + mask_f = msa_attention_mask.to(a.dtype) + n_valid = (mask_f @ mask_f.transpose(-1, -2)).unsqueeze(-1).clamp(min=1.0) + if self._chunk_size is None: + outer = torch.einsum("bimc,bjmd->bijcd", a, b).flatten(-2) + if self.divide_outer_before_proj: + return self.Wout(outer / n_valid) + return self.Wout(outer) / n_valid + # Chunk along the left (i) axis so the peak einsum intermediate is + # [B, chunk, L, c, d] instead of [B, L, L, c, d]. + L = a.shape[1] + out_chunks: list[Tensor] = [] + for s in range(0, L, self._chunk_size): + e = min(s + self._chunk_size, L) + outer_chunk = torch.einsum("bimc,bjmd->bijcd", a[:, s:e], b).flatten(-2) + if self.divide_outer_before_proj: + out_chunks.append(self.Wout(outer_chunk / n_valid[:, s:e])) + else: + out_chunks.append(self.Wout(outer_chunk) / n_valid[:, s:e]) + return torch.cat(out_chunks, dim=1) + + +class ESMFold2MSAPairWeightedAveraging(nn.Module): + """Pair-biased MSA row update (AF3 Supplement Algorithm 10).""" + + def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32) -> None: + super().__init__() + self.n_heads = n_heads + self.head_width = head_width + self.norm_single = ESMFold2LayerNorm(d_msa) + self.compute_bias = nn.Sequential( + ESMFold2LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False) + ) + self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) + self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) + self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) + + def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor) -> Tensor: + """ + Args: + msa_repr: [B, L, M, d_msa] + pair_repr: [B, L, L, d_pair] + pair_attention_mask:[B, L, L] + Returns: + [B, L, M, d_msa] + """ + B, L, M, _ = msa_repr.shape + h, dh = self.n_heads, self.head_width + + msa_normed = self.norm_single(msa_repr.float()).to(msa_repr.dtype) + bias = self.compute_bias[1](self.compute_bias[0](pair_repr.float()).to(pair_repr.dtype)) # [B, L, L, n_heads] + bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) + attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j + + v = self.Wv(msa_normed).reshape(B, L, M, h, dh) + gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) + + output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) + return self.Wout(output.reshape(B, L, M, h * dh)) + + +_CONFIDENCE_EPS = 1e-6 _NONPOLYMER_ID = 4 -class ConfidenceHead(nn.Module): +@dataclass +class ESMFold2Output(ModelOutput): + """ + Output of [`ESMFold2Model`]. All confidence scores are on a 0-1 scale; per-sample tensors + have a leading `num_diffusion_samples` axis. + + Args: + distogram_logits (`torch.FloatTensor` of shape `(batch_size, num_tokens, num_tokens, distogram_bins)`): + Predicted distance-distribution logits over residue pairs (RNG-independent; no diffusion sampling). + sample_atom_coords (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, 3)`): + Predicted all-atom Cartesian coordinates for each diffusion sample. + plddt_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, num_plddt_bins)`): + Per-atom pLDDT bin logits. + plddt (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens)`): + Per-residue predicted lDDT confidence. + plddt_per_atom (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms)`): + Per-atom predicted lDDT confidence. + plddt_ca (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens)`): + Predicted lDDT at the representative (Cα) atom of each token. + complex_plddt (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Mean pLDDT over all atoms of the complex. + complex_iplddt (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Interface-weighted complex pLDDT. + pae_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens, num_pae_bins)`): + Predicted-aligned-error bin logits. + pae (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens)`): + Expected predicted aligned error (Å) for each residue pair. + pde_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens, num_pde_bins)`): + Predicted-distance-error bin logits. + pde (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens)`): + Expected predicted distance error (Å) for each residue pair. + resolved_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, 2)`): + Per-atom resolved/unresolved logits. + ptm (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Predicted TM-score for each sample. + iptm (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Predicted interface TM-score for each sample. + pair_chains_iptm (`torch.FloatTensor` of shape `(num_diffusion_samples, num_chains, num_chains)`): + Predicted interface TM-score for each ordered chain pair. + """ + + distogram_logits: Tensor | None = None + sample_atom_coords: Tensor | None = None + plddt_logits: Tensor | None = None + plddt: Tensor | None = None + plddt_per_atom: Tensor | None = None + plddt_ca: Tensor | None = None + complex_plddt: Tensor | None = None + complex_iplddt: Tensor | None = None + pae_logits: Tensor | None = None + pae: Tensor | None = None + pde_logits: Tensor | None = None + pde: Tensor | None = None + resolved_logits: Tensor | None = None + ptm: Tensor | None = None + iptm: Tensor | None = None + pair_chains_iptm: Tensor | None = None + + +class ESMFold2ConfidenceHead(nn.Module): """Predicts pLDDT, PAE, PDE, resolved-atom probability and distogram bins.""" boundaries: Tensor - def __init__(self, config: "ESMFold2Config") -> None: + def __init__(self, config: ESMFold2Config) -> None: super().__init__() ch = config.confidence_head d_single = config.d_single @@ -63,7 +2420,7 @@ def __init__(self, config: "ESMFold2Config") -> None: self.register_buffer("boundaries", boundaries) self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) - self.s_norm = nn.LayerNorm(d_single, dtype=torch.float32) + self.s_norm = ESMFold2LayerNorm(d_single) self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) @@ -71,26 +2428,26 @@ def __init__(self, config: "ESMFold2Config") -> None: self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) - self.s_inputs_norm = nn.LayerNorm(d_inputs, dtype=torch.float32) - self.z_norm = nn.LayerNorm(d_pair, dtype=torch.float32) + self.s_inputs_norm = ESMFold2LayerNorm(d_inputs) + self.z_norm = ESMFold2LayerNorm(d_pair) - self.row_attention_pooling = RowAttentionPooling(d_pair=d_pair, d_single=d_single) + self.row_attention_pooling = ESMFold2RowAttentionPooling(d_pair=d_pair, d_single=d_single) pf = ch.folding_trunk - self.folding_trunk = FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) + self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) # Heads. - self.plddt_ln = nn.LayerNorm(d_single, dtype=torch.float32) + self.plddt_ln = ESMFold2LayerNorm(d_single) max_atoms_per_token = 23 self.plddt_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)) - self.pae_ln = nn.LayerNorm(d_pair, dtype=torch.float32) + self.pae_ln = ESMFold2LayerNorm(d_pair) self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) - self.pde_ln = nn.LayerNorm(d_pair, dtype=torch.float32) + self.pde_ln = ESMFold2LayerNorm(d_pair) self.pde_head = nn.Linear(d_pair, ch.num_pde_bins, bias=False) - self.resolved_ln = nn.LayerNorm(d_single, dtype=torch.float32) + self.resolved_ln = ESMFold2LayerNorm(d_single) # 2 = resolved logits ([unresolved, resolved]). self.resolved_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, 2)) @@ -178,7 +2535,7 @@ def forward( atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) plddt = plddt_sum / atom_count.clamp(min=1e-6) - complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (atom_mask_f.sum(dim=-1) + _EPS) + complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (atom_mask_f.sum(dim=-1) + _CONFIDENCE_EPS) expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) @@ -193,7 +2550,7 @@ def forward( ) iplddt_weight_atoms = gather_token_to_atom(iplddt_weight.unsqueeze(-1), atom_to_token_m).squeeze(-1) atom_iplddt_w = atom_mask_f * iplddt_weight_atoms - complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (atom_iplddt_w.sum(dim=-1) + _EPS) + complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (atom_iplddt_w.sum(dim=-1) + _CONFIDENCE_EPS) plddt_ca = plddt_per_atom.gather(1, rep_idx_m) @@ -222,11 +2579,11 @@ def forward( tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1) pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) - ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (pair_mask_2d.sum(dim=-1) + _EPS) + ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (pair_mask_2d.sum(dim=-1) + _CONFIDENCE_EPS) ptm = ptm_per_row.max(dim=-1).values inter_chain_mask = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() * pair_mask_2d - iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (inter_chain_mask.sum(dim=-1) + _EPS) + iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (inter_chain_mask.sum(dim=-1) + _CONFIDENCE_EPS) iptm = iptm_per_row.max(dim=-1).values max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 @@ -239,7 +2596,7 @@ def forward( for c2 in range(n_chains): chain_c2 = (expanded_asym == c2).float() * mask_f pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) - denom = pair_m.sum(dim=(-1, -2)) + _EPS + denom = pair_m.sum(dim=(-1, -2)) + _CONFIDENCE_EPS pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(dim=(-1, -2)) / denom return { @@ -264,7 +2621,62 @@ def _inverse_softplus(value: float) -> float: return value + math.log(-math.expm1(-value)) -class ESMFold2Model(PreTrainedModel): +class ESMFold2PreTrainedModel(PreTrainedModel): + """Base class for ESMFold2 — declares the loading and weight-initialization behaviour.""" + + config_class = ESMFold2Config + base_model_prefix = "esmfold2" + main_input_name = "token_index" + _no_split_modules = [ + "ESMCLayer", + "ESMFold2PairUpdateBlock", + "ESMFold2AtomEncoder", + "ESMFold2AtomDecoder", + "ESMFold2DiffusionTransformer", + ] + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + # The Fourier noise-embedding frequencies/phases are random Gaussian features whose + # precision drives the diffusion conditioning; keep them fp32 even under dtype=bf16. + _keep_in_fp32_modules_strict = ["fourier"] + _supports_sdpa = True + _supports_flash_attn = True + _supports_attention_backend = True + + def _init_weights(self, module): + # The base initializer handles Linear/Embedding/LayerNorm; below we (re)apply the + # few non-default inits the architecture needs (adaLN-Zero gates, identity/recurrent + # parcae params, zeroed projections). These live here, not in submodule __init__s, + # so they survive `post_init()` and are not wastefully run before weight loading. + # The `init.*` helpers are load-flag aware (they no-op on already-loaded weights). + super()._init_weights(module) + if isinstance(module, ESMFold2Model): + init.eye_(module.parcae_readout.weight) + init.eye_(module.parcae_b_cont) + init.zeros_(module.parcae_log_a) + parcae_delta_init = -math.log(math.sqrt(1.0 / 5.0)) + init.constant_(module.parcae_log_delta, _inverse_softplus(parcae_delta_init)) + elif isinstance(module, ESMFold2ConfidenceHead): + init.zeros_(module.plddt_weight) + init.zeros_(module.resolved_weight) + elif isinstance(module, ESMFold2AdaptiveLayerNorm): + init.ones_(module.s_scale) + elif isinstance(module, ESMFold2SWAAtomBlock): + init.zeros_(module.adaln_modulation[1].weight) + elif isinstance(module, ESMFold2AttentionPairBias): + if getattr(module, "out_gate", None) is not None: + init.zeros_(module.out_gate.weight) + init.constant_(module.out_gate.bias, -2.0) + elif isinstance(module, ESMFold2ConditionedTransitionBlock): + if getattr(module, "output_gate", None) is not None: + init.zeros_(module.output_gate.weight) + init.constant_(module.output_gate.bias, -2.0) + elif isinstance(module, ESMFold2DiffusionModule): + init.zeros_(module.s_to_token.weight) + elif isinstance(module, ESMFold2LanguageModelShim): + init.zeros_(module.base_z_combine) + + +class ESMFold2Model(ESMFold2PreTrainedModel): """ESMFold2 — all-atom structure prediction with an ESMC PLM backbone. This is the standard released ESMFold2 architecture (uses a linear- @@ -287,62 +2699,58 @@ class ESMFold2Model(PreTrainedModel): L≈2k on an 80 GB GPU. Pass ``None`` for faster inference at L<600. """ - config_class = ESMFold2Config - _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] - # The Fourier noise-embedding frequencies/phases are random Gaussian features whose - # precision drives the diffusion conditioning; keep them fp32 even under dtype=bf16. - _keep_in_fp32_modules_strict = ["fourier"] - _supports_sdpa = True - _supports_flash_attn = True - _supports_attention_backend = True - def __init__(self, config: ESMFold2Config) -> None: super().__init__(config) d_inputs = config.inputs.d_inputs d_pair = config.d_pair - self.inputs_embedder = InputsEmbedder(config) + self.inputs_embedder = ESMFold2InputsEmbedder(config) self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False) - self.rel_pos = ResIdxAsymIdSymIdEntityIdEncoding( + self.rel_pos = ESMFold2ResIdxAsymIdSymIdEntityIdEncoding( n_relative_residx_bins=config.n_relative_residx_bins, n_relative_chain_bins=config.n_relative_chain_bins, d_pair=d_pair, ) self.token_bonds = nn.Linear(1, d_pair, bias=False) - self.language_model = LanguageModelShim(d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers) - self._esmc: nn.Module | None = None + self.language_model = ESMFold2LanguageModelShim( + d_z=d_pair, d_model=config.esmc_config.d_model, num_layers=config.esmc_config.n_layers + ) + # The ESMC backbone is a bundled submodule: built here with random weights + # (instant, no I/O), then populated by the parent's from_pretrained like any + # other composite sub-model (its weights live under ``esmc.*`` in the ESMFold2 + # checkpoint). It is frozen in effect — ``forward`` detaches its hidden states + # before they enter the trunk, so no gradient ever reaches it — and the bf16 + # autocast in ``_compute_lm_hidden_states`` reproduces the LM precision regime. + self.esmc = AutoModel.from_config(config.esmc_config) pf = config.folding_trunk - self.folding_trunk = FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) + self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) if config.lm_encoder.enabled: - self.lm_encoder: FoldingTrunk | None = FoldingTrunk( + self.lm_encoder: ESMFold2FoldingTrunk | None = ESMFold2FoldingTrunk( n_layers=config.lm_encoder.n_layers, d_pair=d_pair, expansion_ratio=4 ) else: self.lm_encoder = None - self.parcae_input_norm = nn.LayerNorm(d_pair, dtype=torch.float32) + # parcae linear-recurrence params (allocated here; initialized in _init_weights): + # log_a -> 0, log_delta -> a fixed decay constant, b_cont -> identity, readout -> identity. + self.parcae_input_norm = ESMFold2LayerNorm(d_pair) self.parcae_log_a = nn.Parameter(torch.zeros(d_pair)) - parcae_decay_init = math.sqrt(1.0 / 5.0) - parcae_delta_init = -math.log(parcae_decay_init) - self.parcae_log_delta = nn.Parameter( - torch.full((d_pair,), _inverse_softplus(parcae_delta_init), dtype=torch.float32) - ) - self.parcae_b_cont = nn.Parameter(torch.eye(d_pair)) + self.parcae_log_delta = nn.Parameter(torch.empty(d_pair, dtype=torch.float32)) + self.parcae_b_cont = nn.Parameter(torch.empty(d_pair, d_pair)) self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) - nn.init.eye_(self.parcae_readout.weight) - self.parcae_coda = FoldingTrunk(n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4) + self.parcae_coda = ESMFold2FoldingTrunk(n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4) # Heads -------------------------------------------------------------- - self.structure_head = DiffusionStructureHead(config) + self.structure_head = ESMFold2DiffusionStructureHead(config) self.distogram_head = nn.Linear(d_pair, config.structure_head.distogram_bins, bias=True) - self.confidence_head = ConfidenceHead(config) + self.confidence_head = ESMFold2ConfidenceHead(config) msa_cfg = config.msa_encoder self.msa_encoder = None if msa_cfg.enabled: - self.msa_encoder = MSAEncoder( + self.msa_encoder = ESMFold2MSAEncoder( d_msa=msa_cfg.d_msa, d_pair=d_pair, d_inputs=d_inputs, @@ -352,78 +2760,17 @@ def __init__(self, config: ESMFold2Config) -> None: msa_head_width=msa_cfg.msa_head_width, ) - # SWA3DRoPEAttention modules live deep in the atom encoders/decoders and + # ESMFold2SWA3DRoPEAttention modules live deep in the atom encoders/decoders and # are built from explicit dims, so give each a handle to the model config: # their forward dispatches the plain-attention core through the v5 # attention interface (config._attn_implementation), staying live under # set_attn_implementation() since the config object is shared. for module in self.modules(): - if isinstance(module, SWA3DRoPEAttention): + if isinstance(module, ESMFold2SWA3DRoPEAttention): module.config = self.config self.post_init() - def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None: - """Load the ESMC LM backbone. ``precision``: ``"bf16"`` (default) or ``"fp32"``.""" - # Resolve the ESMC backbone through the Auto registry (model_type "esmc" - # -> ESMCModel) rather than a hard cross-model import. ESMC is a shared, - # frozen backbone loaded from its own repo (`esmc_id`), not bundled here. - from ...models.auto.modeling_auto import AutoModel - - dtype_map = { - "bf16": torch.bfloat16, - "fp32": torch.float32, - } - if precision not in dtype_map: - raise ValueError(f"precision must be one of {list(dtype_map)}, got {precision!r}") - dtype = dtype_map[precision] - - # Load directly in the target dtype so the ~25GB fp32 6B backbone is never - # materialised (it would not fit alongside the trunk on a 24GB GPU). The - # backbone is run exactly as the reference does — plain bf16 weights under a - # bf16 autocast (see ``_compute_lm_hidden_states``) — so no per-parameter - # dtype fix-up is needed here. - esmc = AutoModel.from_pretrained(esmc_model_path, dtype=dtype).to(device=self.device, dtype=dtype).eval() - for p in esmc.parameters(): - p.requires_grad_(False) - - self._esmc = esmc - - @classmethod - def from_pretrained(cls, pretrained_model_name_or_path, *args, load_esmc: bool = True, **kwargs): - if cls is ESMFold2Model and "config" not in kwargs: - config = ESMFold2Config.from_pretrained(pretrained_model_name_or_path, **kwargs) - kwargs["config"] = config - # Pop the precision knob before forwarding to the HF loader. - esmc_precision = kwargs.pop("esmc_precision", "bf16") - model = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs) - # Match the reference's bf16-rounded trunk norm weights (see the method docstring). - # Done before ``load_esmc`` so only the trunk is touched, never the separately - # loaded ESMC backbone (``self._esmc`` is still ``None`` here). - model._round_trunk_norms_to_bf16() - if load_esmc: - model.load_esmc(model.config.esmc_id, precision=esmc_precision) - return model - - def _round_trunk_norms_to_bf16(self) -> None: - """Round the trunk LayerNorm weights to bf16 (stored fp32) for a bf16 trunk. - - The trunk LayerNorms are kept fp32 (``nn.LayerNorm(dtype=torch.float32)``) so the - dtype-honest call sites can feed them an upcast input. The reference, however, - loads the whole trunk in bf16 and recovers fp32 only for the norm *compute* (via - autocast) — i.e. its norm *weights* are bf16-ROUNDED. We reproduce that so a bf16 - trunk matches the checkpoint's training regime: round each fp32 norm weight to - bf16 and back. No-op for an fp32 trunk (the fp32 reference keeps full precision), - and the values stay fp32-dtype so the fp32-compute call sites are unaffected. - """ - if not any(p.dtype == torch.bfloat16 for p in self.parameters()): - return - for module in self.modules(): - if isinstance(module, nn.LayerNorm) and module.weight is not None and module.weight.dtype == torch.float32: - module.weight.data = module.weight.data.bfloat16().float() - if module.bias is not None: - module.bias.data = module.bias.data.bfloat16().float() - def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = None) -> None: """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" import torch._dynamo @@ -437,17 +2784,11 @@ def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = dynamic = mode == "dynamic_seqlen" kwargs: dict = {"dynamic": dynamic} - from .modeling_esmfold2_common import ( - DiffusionModule, - DiffusionTransformer, - PairUpdateBlock, - ) - compile_targets = ( - PairUpdateBlock, - DiffusionTransformer, - DiffusionModule, - MSAEncoderBlock, + ESMFold2PairUpdateBlock, + ESMFold2DiffusionTransformer, + ESMFold2DiffusionModule, + ESMFold2MSAEncoderBlock, ) def _maybe_compile(module: nn.Module) -> None: @@ -473,17 +2814,16 @@ def _compute_lm_hidden_states( mol_type: Tensor, tok_mask: Tensor, ) -> Tensor: - assert self._esmc is not None # Run the frozen ESMC backbone exactly as the reference does: plain weights # under a bf16 autocast (the fork's ``_lm_precision_context``). For a bf16 # backbone this puts norms/softmax in fp32 and matmuls/rotary in bf16, # reproducing the checkpoint's training regime bit-for-bit; disabled (a # no-op) for an fp32 backbone. This autocast is scoped to the backbone only # — the trunk stays dtype-honest. - use_amp = next(self._esmc.parameters()).dtype == torch.bfloat16 + use_amp = next(self.esmc.parameters()).dtype == torch.bfloat16 with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=use_amp): return compute_lm_hidden_states( - self._esmc, + self.esmc, input_ids, asym_id, residue_index, @@ -555,7 +2895,6 @@ def _run_one_loop( return z - @torch.inference_mode() def forward( self, token_index: Tensor, @@ -586,7 +2925,7 @@ def forward( num_diffusion_samples: int | None = None, num_sampling_steps: int | None = None, **kwargs, - ) -> dict[str, Tensor]: + ) -> ESMFold2Output: tok_mask = token_attention_mask atm_mask = atom_attention_mask disto_idx = distogram_atom_idx @@ -651,7 +2990,7 @@ def forward( token_bonds_encoding = self.token_bonds(token_bonds.to(self.token_bonds.weight.dtype)) z_init = z_init + relative_position_encoding + token_bonds_encoding - if lm_hidden_states is None and input_ids is not None and self._esmc is not None: + if lm_hidden_states is None and input_ids is not None: lm_hidden_states = self._compute_lm_hidden_states(input_ids, asym_id, residue_index, mol_type, tok_mask) lm_z: Tensor | None = None if lm_hidden_states is not None: @@ -675,7 +3014,7 @@ def forward( if msa_attention_mask is not None else tok_mask[:, :, None].expand(-1, -1, M).float() ) - # Bias-free MSAEncoder.embed requires zeroed padding. + # Bias-free ESMFold2MSAEncoder.embed requires zeroed padding. msa_oh = msa_oh * msa_attn.unsqueeze(-1) hd = ( has_deletion.permute(0, 2, 1).float() @@ -738,9 +3077,8 @@ def forward( ) sample_coords = structure_output["sample_atom_coords"] - assert sample_coords is not None - output: dict[str, Tensor] = {"distogram_logits": distogram_logits} - output["sample_atom_coords"] = sample_coords + if sample_coords is None: + raise RuntimeError("The diffusion structure head did not return sampled coordinates.") confidence_output = self.confidence_head( s_inputs=x_inputs.detach(), @@ -756,34 +3094,39 @@ def forward( relative_position_encoding=relative_position_encoding.detach(), token_bonds_encoding=token_bonds_encoding.detach(), ) - output.update(confidence_output) - output["atom_pad_mask"] = atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask - output["residue_index"] = residue_index - output["entity_id"] = entity_id - return output + + return ESMFold2Output( + distogram_logits=distogram_logits, + sample_atom_coords=sample_coords, + **confidence_output, + ) @torch.no_grad() - def infer_protein(self, seq: str, **forward_kwargs) -> dict: - from .protein_utils import OUTPUT_TO_PDB_FEATURE_KEYS, prepare_protein_features + def infer_protein(self, seq: str, **forward_kwargs) -> ESMFold2Output: + from .protein_utils import prepare_protein_features features = prepare_protein_features(seq) features = {k: v.to(self.device) for k, v in features.items()} - output = self(**features, **forward_kwargs) - for k in OUTPUT_TO_PDB_FEATURE_KEYS: - output[k] = features[k] - return output + return self(**features, **forward_kwargs) + @torch.no_grad() def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str: - return self.output_to_pdb(self.infer_protein(seq, **forward_kwargs)) + from .protein_utils import output_to_pdb, prepare_protein_features + + features = prepare_protein_features(seq) + features = {k: v.to(self.device) for k, v in features.items()} + output = self(**features, **forward_kwargs) + return output_to_pdb(output, features) @staticmethod - def output_to_pdb(output: dict) -> str: + def output_to_pdb(output: ESMFold2Output, features: dict[str, Tensor]) -> str: + """Render a PDB string from an [`ESMFold2Output`] and the input ``features`` it was produced from.""" from .protein_utils import output_to_pdb as _output_to_pdb - return _output_to_pdb(output) + return _output_to_pdb(output, features) -class MSAEncoderBlock(nn.Module): +class ESMFold2MSAEncoderBlock(nn.Module): """One MSA encoder block: OPM into pair, MSA pair-weighted averaging, triangle update.""" def __init__( @@ -797,13 +3140,15 @@ def __init__( ) -> None: super().__init__() self.is_final_block = is_final_block - self.outer_product_mean = OuterProductMean(d_msa, d_hidden, d_pair) + self.outer_product_mean = ESMFold2OuterProductMean(d_msa, d_hidden, d_pair) if not is_final_block: - self.msa_pair_weighted_averaging = MSAPairWeightedAveraging(d_msa, d_pair, n_heads_msa, msa_head_width) - self.msa_transition = Transition(d_msa, expansion_ratio=4) - self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) - self.pair_transition = Transition(d_pair, expansion_ratio=4) + self.msa_pair_weighted_averaging = ESMFold2MSAPairWeightedAveraging( + d_msa, d_pair, n_heads_msa, msa_head_width + ) + self.msa_transition = ESMFold2Transition(d_msa, expansion_ratio=4) + self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=4) def set_chunk_size(self, chunk_size: int | None) -> None: self.outer_product_mean.set_chunk_size(chunk_size) @@ -830,8 +3175,8 @@ def forward( return m, pair -class MSAEncoder(nn.Module): - """Stack of [`MSAEncoderBlock`] layers that conditions the pair on an MSA.""" +class ESMFold2MSAEncoder(nn.Module): + """Stack of [`ESMFold2MSAEncoderBlock`] layers that conditions the pair on an MSA.""" def __init__( self, @@ -848,7 +3193,7 @@ def __init__( self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) self.blocks = nn.ModuleList( [ - MSAEncoderBlock( + ESMFold2MSAEncoderBlock( d_msa=d_msa, d_pair=d_pair, d_hidden=d_hidden, @@ -883,4 +3228,4 @@ def forward( return x_pair -__all__ = ["ESMFold2Model"] +__all__ = ["ESMFold2Model", "ESMFold2PreTrainedModel"] diff --git a/src/transformers/models/esmfold2/modeling_esmfold2_common.py b/src/transformers/models/esmfold2/modeling_esmfold2_common.py deleted file mode 100644 index e00f87ff8081..000000000000 --- a/src/transformers/models/esmfold2/modeling_esmfold2_common.py +++ /dev/null @@ -1,2273 +0,0 @@ -# Copyright 2026 Biohub. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -"""Shared building blocks for ESMFold2 HuggingFace model variants.""" - -from __future__ import annotations - -from collections.abc import Callable -from functools import partial - -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch import Tensor -from torch.utils.checkpoint import checkpoint - - -try: - from flash_attn import ( - flash_attn_func, - flash_attn_varlen_func, - ) - from flash_attn.bert_padding import ( - index_first_axis, - pad_input, - ) - - FLASH_ATTN_AVAILABLE = True -except ImportError: - flash_attn_func = None - flash_attn_varlen_func = None - index_first_axis = None - pad_input = None - FLASH_ATTN_AVAILABLE = False - -from ...integrations import use_kernel_forward_from_hub -from ...modeling_utils import ALL_ATTENTION_FUNCTIONS -from ...processing_utils import Unpack -from ...utils import TransformersKwargs -from .configuration_esmfold2 import ESMFold2Config - - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- -CHAR_VOCAB_SIZE: int = 64 -MAX_CHARS: int = 4 -XYZ_DIMS: int = 3 -MAX_ATOMIC_NUMBER: int = 128 - -# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 -ATOM_FEATURE_DIM: int = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS - - -NUM_RES_TYPES: int = 33 - -_EPS = 1e-5 - -# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory -# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; -# chunk=64 leaves headroom for the largest foldbench targets). Override via -# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for -# short L but OOM-prone past ~600). -_DEFAULT_CHUNK_SIZE = 64 - - -# =========================================================================== -# Atom-token utilities -# =========================================================================== - - -def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: - """Broadcast per-token features to per-atom features using gather. - - Args: - token_features: [B, L, d] - atom_to_token_idx: [B, A] int64 - - Returns: - [B, A, d] - """ - idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) - return torch.gather(token_features, 1, idx) - - -def scatter_atom_to_token( - atom_features: Tensor, - atom_to_token_idx: Tensor, - n_tokens: int, - atom_mask: Tensor | None = None, -) -> Tensor: - """Aggregate per-atom features to per-token features (mean). - - Args: - atom_features: [B, A, d] - atom_to_token_idx: [B, A] int64 - n_tokens: L - atom_mask: [B, A] bool - - Returns: - [B, L, d] - """ - B, A, d = atom_features.shape - n_out = n_tokens - idx = atom_to_token_idx - if atom_mask is not None: - idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) - n_out = n_tokens + 1 - idx_expanded = idx.unsqueeze(-1).expand(B, A, d) - out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) - out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) - return out[:, :n_tokens, :] - - -def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: - """Gather representative atom coordinates for each token. - - Args: - coords: [B, A, 3] - rep_atom_idx: [B, L] int64 - - Returns: - [B, L, 3] - """ - idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) - return torch.gather(coords, 1, idx) - - -def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: - """Compute local atom index within each token (vectorised). - - Atoms belonging to the same token are contiguous, so this computes a - running count that resets at each token boundary. - - Args: - atom_to_token: [B, A] flat index mapping each atom to its token. - - Returns: - [B, A] tensor with values in [0, max_atoms_per_token - 1]. - """ - same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) - ones = torch.ones_like(atom_to_token) - cumsum = torch.cumsum(ones, dim=-1) - group_start = cumsum.masked_fill(same_as_prev, 0) - group_start = torch.cummax(group_start, dim=-1).values - return cumsum - group_start - - -def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: - """Expected value of a categorical distribution over evenly-spaced bins. - - Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. - - Args: - logits: [..., n_bins] - start: left boundary - end: right boundary - - Returns: - [...] expected value - """ - n_bins = logits.shape[-1] - edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) - v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] - return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) - - -# =========================================================================== -# TransitionLayer (used in DiffusionConditioning) -# =========================================================================== - - -class TransitionLayer(nn.Module): - """SwiGLU transition: norm -> a_proj, b_proj -> silu(a)*b -> out_proj.""" - - def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: - super().__init__() - hidden = n * d_model - self.norm = nn.LayerNorm(d_model, eps=eps, dtype=torch.float32) - self.a_proj = nn.Linear(d_model, hidden, bias=False) - self.b_proj = nn.Linear(d_model, hidden, bias=False) - self.out_proj = nn.Linear(hidden, d_model, bias=False) - - def forward(self, x: Tensor) -> Tensor: - x = self.norm(x.float()).to(x.dtype) - a = self.a_proj(x) - b = self.b_proj(x) - return self.out_proj(F.silu(a) * b) - - -# =========================================================================== -# AdaptiveLayerNorm (used in DiffusionTransformer) -# =========================================================================== - - -class AdaptiveLayerNorm(nn.Module): - """Adaptive layer normalization (adaLN-Zero).""" - - def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: - super().__init__() - self.d_model = d_model - self.d_cond = d_cond - self.eps = eps - self.s_scale = nn.Parameter(torch.ones(d_cond)) - self.s_gate = nn.Linear(d_cond, d_model, bias=True) - self.s_shift = nn.Linear(d_cond, d_model, bias=False) - - def forward(self, a: Tensor, s: Tensor) -> Tensor: - a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) - s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) - # gate/shift in bf16 (matches the reference's autocast); a_norm is fp32 so the - # affine promotes to fp32, then downcast for the next op. - gate = torch.sigmoid(self.s_gate(s_norm)) - shift = self.s_shift(s_norm) - return (gate * a_norm + shift).to(a.dtype) - - -# =========================================================================== -# FourierEmbedding -# =========================================================================== - - -class FourierEmbedding(nn.Module): - """Fourier embedding: cos(2*pi*(t*w + b)).""" - - w: Tensor - b: Tensor - - def __init__(self, c: int) -> None: - super().__init__() - self.c = c - self.register_buffer("w", torch.randn(c)) - self.register_buffer("b", torch.randn(c)) - - def forward(self, t_hat: Tensor) -> Tensor: - # w/b are kept fp32 (ESMFold2Model._keep_in_fp32_modules_strict), so the random - # frequencies/phases — and the cos embedding — are computed at full precision. - t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) - return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) - - -# =========================================================================== -# SwiGLU / SwiGLUMLP -# =========================================================================== - - -class SwiGLU(nn.Module): - """SwiGLU with packed w12 and output w3.""" - - def __init__( - self, - in_features: int, - hidden_features: int, - out_features: int | None = None, - bias: bool = True, - ) -> None: - super().__init__() - out_features = out_features or in_features - self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) - self.w3 = nn.Linear(hidden_features, out_features, bias=bias) - self.hidden_features = hidden_features - - def forward(self, x: Tensor) -> Tensor: - x12 = self.w12(x) - x1, x2 = x12.split(self.hidden_features, dim=-1) - hidden = F.silu(x1) * x2 - return self.w3(hidden) - - -class SwiGLUMLP(SwiGLU): - """SwiGLU MLP with packed weights, no bias.""" - - def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: - hidden = expansion_ratio * d_model - super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) - - -# =========================================================================== -# SWA Atom Attention components -# =========================================================================== - - -# Copied from transformers.models.esm.modeling_esm.rotate_half -def rotate_half(x): - """Rotates half the hidden dims of the input.""" - x1 = x[..., : x.shape[-1] // 2] - x2 = x[..., x.shape[-1] // 2 :] - return torch.cat((-x2, x1), dim=-1) - - -def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: - """Apply RoPE with batch-dependent cos/sin. - - Args: - x: [B, L, H, D] - cos: [B, L, D/2] - sin: [B, L, D/2] - """ - ro_dim = cos.shape[-1] * 2 - cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) - sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) - return torch.cat( - [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], - dim=-1, - ) - - -@torch.compiler.disable -def build_3d_rope( - ref_pos: Tensor, - ref_space_uid: Tensor, - head_dim: int, - n_spatial_per_axis: int = 4, - n_uid_pairs: int = 2, - spatial_base_freq: float = 10000.0, - uid_base_freq: float = 10.0, -) -> tuple[Tensor, Tensor]: - """Build cos/sin for 3D RoPE + UID RoPE.""" - device = ref_pos.device - B, N = ref_pos.shape[:2] - half_dim = head_dim // 2 - n_spatial_total = 3 * n_spatial_per_axis - - spatial_inv_freq = 1.0 / ( - spatial_base_freq - ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) - ) - uid_inv_freq = 1.0 / ( - uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) - ) - - pos_f32 = ref_pos.float() - spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) - spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) - - uid_f32 = ref_space_uid.float() - uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) - - n_active = n_spatial_total + n_uid_pairs - freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) - - if n_active < half_dim: - padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) - freqs = torch.cat([freqs, padding], dim=-1) - - cos = freqs.cos().to(torch.bfloat16) - sin = freqs.sin().to(torch.bfloat16) - return cos, sin - - -def qk_norm(x: Tensor) -> Tensor: - return F.rms_norm(x, (x.size(-1),)) - - -# =========================================================================== -# SwiGLUFFN (atom transformer blocks) -# =========================================================================== - - -class SwiGLUFFN(nn.Module): - """SwiGLU FFN with rounded hidden size for hardware alignment.""" - - def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: - super().__init__() - hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 - self.w_up = nn.Linear(d_model, 2 * hidden_size, bias=False) - self.w_down = nn.Linear(hidden_size, d_model, bias=False) - - def forward(self, x: Tensor) -> Tensor: - x = x.to(self.w_up.weight.dtype) - x1, x2 = self.w_up(x).chunk(2, dim=-1) - return self.w_down(F.silu(x1) * x2) - - -# =========================================================================== -# SWA3DRoPEAttention -# =========================================================================== - - -# Copied from transformers.models.llama.modeling_llama.repeat_kv -def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: - """ - This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, - num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) - """ - batch, num_key_value_heads, slen, head_dim = hidden_states.shape - if n_rep == 1: - return hidden_states - hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim) - return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) - - -# Copied from transformers.models.llama.modeling_llama.eager_attention_forward -def eager_attention_forward( - module: nn.Module, - query: torch.Tensor, - key: torch.Tensor, - value: torch.Tensor, - attention_mask: torch.Tensor | None, - scaling: float, - dropout: float = 0.0, - **kwargs: Unpack[TransformersKwargs], -): - key_states = repeat_kv(key, module.num_key_value_groups) - value_states = repeat_kv(value, module.num_key_value_groups) - - attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling - if attention_mask is not None: - attn_weights = attn_weights + attention_mask - - attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) - attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) - attn_output = torch.matmul(attn_weights, value_states) - attn_output = attn_output.transpose(1, 2).contiguous() - - return attn_output, attn_weights - - -class SWA3DRoPEAttention(nn.Module): - """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. - - The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention - interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), - with the sliding window expressed as an additive attention mask. The custom - flash-attention path (native bidirectional ``window_size``, plus varlen for - packed inputs) is kept as an opt-in backend, selected when - ``_attn_implementation == "flash_attention_2"``. ``config`` is attached by - the parent ``ESMFold2Model`` after construction; it is ``None`` (→ ``sdpa``) - when the module is used standalone. - """ - - def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: - super().__init__() - self.config = None - self.n_heads = n_heads - self.head_dim = d_model // n_heads - self.scale = self.head_dim**-0.5 - self.half_window = half_window - # No grouped-query attention; identity repeat keeps the interface happy. - self.num_key_value_groups = 1 - # Bidirectional encoder: never let the sdpa/flash interface default to - # causal masking when attention_mask happens to be None. - self.is_causal = False - - self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) - self.out_proj = nn.Linear(d_model, d_model, bias=False) - self.gate_proj = nn.Linear(d_model, d_model, bias=False) - - def forward(self, x: Tensor, attention_params: tuple) -> Tensor: - B, N = x.shape[:2] - cos, sin = attention_params[0], attention_params[1] - - x_input = x - qkv = self.Wqkv(x) - qkv = qkv.view(B, N, 3, self.n_heads, self.head_dim).permute(2, 0, 1, 3, 4) - q, k, v = qkv.unbind(0) - q, k = qk_norm(q), qk_norm(k) - - q = apply_rotary_emb_3d(q, cos, sin) - k = apply_rotary_emb_3d(k, cos, sin) - - input_dtype = q.dtype - if q.dtype not in (torch.float16, torch.bfloat16): - q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() - - attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" - use_flash = attn_impl == "flash_attention_2" and FLASH_ATTN_AVAILABLE - - if use_flash and len(attention_params) > 2: - indices, cu_seqlens, max_seqlen = ( - attention_params[2], - attention_params[3], - attention_params[4], - ) - q_unpad = index_first_axis(q.reshape(-1, self.n_heads, self.head_dim), indices) - k_unpad = index_first_axis(k.reshape(-1, self.n_heads, self.head_dim), indices) - v_unpad = index_first_axis(v.reshape(-1, self.n_heads, self.head_dim), indices) - out_unpad = flash_attn_varlen_func( - q_unpad, - k_unpad, - v_unpad, - cu_seqlens, - cu_seqlens, - max_seqlen, - max_seqlen, - softmax_scale=self.scale, - window_size=(self.half_window, self.half_window), - ) - out = pad_input(out_unpad, indices, B, N) - elif use_flash: - out = flash_attn_func( - q, - k, - v, - softmax_scale=self.scale, - window_size=(self.half_window, self.half_window), - ) - else: - if len(attention_params) > 2: - valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) - valid[attention_params[2]] = True - valid = valid.view(B, N) - else: - valid = torch.ones(B, N, dtype=torch.bool, device=q.device) - rank = torch.cumsum(valid, dim=1) - 1 - within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window - allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) - allowed |= torch.eye(N, dtype=torch.bool, device=q.device) - # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. - attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) - attn_mask = attn_mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(q.dtype).min) - - attention_interface: Callable = eager_attention_forward - if attn_impl != "eager": - attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) - out, _ = attention_interface( - self, - q.transpose(1, 2), - k.transpose(1, 2), - v.transpose(1, 2), - attn_mask, - dropout=0.0, - scaling=self.scale, - ) - out = out * valid.unsqueeze(-1).unsqueeze(-1) - - out = out.to(input_dtype).reshape(B, N, -1) - out = out * torch.sigmoid(self.gate_proj(x_input)) - return self.out_proj(out) - - -# =========================================================================== -# SWAAtomBlock, SWAAtomTransformer -# =========================================================================== - - -def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: - return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift - - -def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: - return x + gate * y - - -class SWAAtomBlock(nn.Module): - """adaLN-Zero + SWA attention + SwiGLU FFN. - - Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight - """ - - def __init__( - self, - d_atom: int, - n_heads: int, - half_window: int = 64, - expansion_ratio: int = 2, - ) -> None: - super().__init__() - adaln_linear = nn.Linear(d_atom, 6 * d_atom, bias=False) - nn.init.zeros_(adaln_linear.weight) - self.adaln_modulation = nn.Sequential(nn.SiLU(), adaln_linear) - - self.attn = SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) - self.ffn = SwiGLUFFN(d_atom, expansion_ratio) - - def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: - mod = self.adaln_modulation(c_l) - if mod.dim() == 2: - mod = mod.unsqueeze(1) - shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) - - attn_input = _rms_adaln(x, scale_a, shift_a) - attn_out = self.attn(attn_input, attention_params) - x = _gated_residual(x, gate_a, attn_out) - - ffn_input = _rms_adaln(x, scale_f, shift_f) - ffn_out = self.ffn(ffn_input) - x = _gated_residual(x, gate_f, ffn_out) - return x - - -class SWAAtomTransformer(nn.Module): - """Stack of SWAAtomBlocks.""" - - def __init__( - self, - d_atom: int = 128, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.swa_window_size = swa_window_size - self.head_dim = d_atom // n_heads - self.spatial_rope_base_frequency = spatial_rope_base_frequency - self.n_spatial_rope_pairs_per_axis = n_spatial_rope_pairs_per_axis - self.n_uid_rope_pairs = n_uid_rope_pairs - self.uid_rope_base_frequency = uid_rope_base_frequency - - self.blocks = nn.ModuleList( - [ - SWAAtomBlock( - d_atom=d_atom, - n_heads=n_heads, - half_window=swa_window_size // 2, - expansion_ratio=expansion_ratio, - ) - for _ in range(n_blocks) - ] - ) - - def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: - return build_3d_rope( - ref_pos=ref_pos, - ref_space_uid=ref_space_uid, - head_dim=self.head_dim, - n_spatial_per_axis=self.n_spatial_rope_pairs_per_axis, - n_uid_pairs=self.n_uid_rope_pairs, - spatial_base_freq=self.spatial_rope_base_frequency, - uid_base_freq=self.uid_rope_base_frequency, - ) - - def forward( - self, - q_l: Tensor, - c_l: Tensor, - attention_params: tuple, - return_intermediates: bool = False, - ) -> Tensor | tuple[Tensor, list[Tensor]]: - intermediates: list[Tensor] = [] - for block in self.blocks: - q_l = block(q_l, c_l, attention_params) - if return_intermediates: - intermediates.append(q_l) - if return_intermediates: - return q_l, intermediates - return q_l - - -# =========================================================================== -# ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) -# =========================================================================== - - -class ESMFold2AtomEncoder(nn.Module): - """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. - - Args: - d_atom: atom hidden dim - d_token: token dim for atom_to_token aggregation - n_blocks, n_heads, swa_window_size, expansion_ratio: transformer params - structure_prediction: if True, creates coords_linear and uses full d_token - spatial_rope_base_frequency, n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs, uid_rope_base_frequency: 3D RoPE config - """ - - def __init__( - self, - d_atom: int = 128, - d_token: int = 768, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - structure_prediction: bool = True, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.d_atom = d_atom - self.d_token = d_token - self.structure_prediction = structure_prediction - - self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) - self.atom_norm = nn.LayerNorm(d_atom, dtype=torch.float32) - - if structure_prediction: - self.coords_linear = nn.Linear(6, d_atom, bias=False) - - self.atom_transformer = SWAAtomTransformer( - d_atom=d_atom, - n_blocks=n_blocks, - n_heads=n_heads, - swa_window_size=swa_window_size, - expansion_ratio=expansion_ratio, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - # Output aggregation: d_token for structure prediction, d_token//2 for inputs - out_dim = d_token if structure_prediction else d_token // 2 - self.atom_to_token_linear = nn.Linear(d_atom, out_dim, bias=False) - - def forward( - self, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, - r_l: Tensor | None = None, - pred_r1: Tensor | None = None, - num_diffusion_samples: int = 1, - return_intermediates: bool = False, - inference_cache: dict | None = None, - ) -> tuple[Tensor, Tensor, Tensor, tuple, list[Tensor]]: - """Returns (a, q, c, attention_params, intermediates). - - ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, - attention indices, n_tokens) across diffusion steps. - """ - B, N = ref_pos.shape[:2] - - layer_cache = None - if inference_cache is not None: - layer_cache = inference_cache.setdefault("atomencoder", {}) - - if layer_cache is None or len(layer_cache) == 0: - atom_feats = torch.cat( - [ - ref_pos, - ref_charge.unsqueeze(-1), - atom_attention_mask.unsqueeze(-1), - ref_element, - ref_atom_name_chars.reshape(B, N, MAX_CHARS * CHAR_VOCAB_SIZE), - ], - dim=-1, - ) - c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( - self.atom_linear.weight.dtype - ) - cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) - cos = cos.repeat_interleave(num_diffusion_samples, 0) - sin = sin.repeat_interleave(num_diffusion_samples, 0) - mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) - seqlens = mask_exp.sum(dim=-1, dtype=torch.int32) - indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() - max_seqlen = int(seqlens.max().item()) - cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) - attention_params = (cos, sin, indices, cu_seqlens, max_seqlen) - n_tokens = int(atom_to_token.max().item()) + 1 - if layer_cache is not None: - layer_cache["c_base"] = c_base - layer_cache["attention_params"] = attention_params - layer_cache["mask_exp"] = mask_exp - layer_cache["n_tokens"] = n_tokens - layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - else: - c_base = layer_cache["c_base"] - attention_params = layer_cache["attention_params"] - mask_exp = layer_cache["mask_exp"] - n_tokens = layer_cache["n_tokens"] - - c = c_base - - q = c - - if self.structure_prediction and r_l is not None: - q = q.repeat_interleave(num_diffusion_samples, 0) - if pred_r1 is None: - pred_r1 = torch.zeros_like(r_l) - r_input = torch.cat([r_l, pred_r1], dim=-1) - r_to_q = self.coords_linear(r_input.to(self.coords_linear.weight.dtype)) - q = q + r_to_q - - c = c.repeat_interleave(num_diffusion_samples, 0) - - result = self.atom_transformer( - q_l=q, - c_l=c, - attention_params=attention_params, - return_intermediates=return_intermediates, - ) - if return_intermediates: - q, intermediates = result - else: - q = result - intermediates = [] - - q_to_a = F.relu(self.atom_to_token_linear(q)) - if layer_cache is not None and "atom_to_token_exp" in layer_cache: - atom_to_token_exp = layer_cache["atom_to_token_exp"] - else: - atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) - - return a, q, c, attention_params, intermediates - - -# =========================================================================== -# ESMFold2AtomDecoder -# =========================================================================== - - -class ESMFold2AtomDecoder(nn.Module): - """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear.""" - - def __init__( - self, - d_atom: int = 128, - d_token: int = 768, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) - - self.atom_transformer = SWAAtomTransformer( - d_atom=d_atom, - n_blocks=n_blocks, - n_heads=n_heads, - swa_window_size=swa_window_size, - expansion_ratio=expansion_ratio, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - self.norm = nn.LayerNorm(d_atom, dtype=torch.float32) - self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) - - def forward( - self, - a_i: Tensor, - q_l: Tensor, - c_l: Tensor, - p_lm: tuple, - atom_to_token: Tensor, - atom_attention_mask: Tensor, - num_diffusion_samples: int = 1, - return_intermediates: bool = False, - ) -> tuple[Tensor, list[Tensor]]: - """Returns (r_update, intermediates).""" - atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - a_to_q = self.token_to_atom_linear(a_i) - a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) - q_l = q_l + a_to_q - - result = self.atom_transformer( - q_l=q_l, - c_l=c_l, - attention_params=p_lm, - return_intermediates=return_intermediates, - ) - if return_intermediates: - q_l, intermediates = result - else: - q_l = result - intermediates = [] - - r_l = self.output_linear(self.norm(q_l.float()).to(q_l.dtype)) - return r_l, intermediates - - -# =========================================================================== -# AttentionPairBias (DiffusionTransformer attention block) -# =========================================================================== - - -class AttentionPairBias(nn.Module): - """Gated multi-head attention with pair bias conditioning.""" - - def __init__( - self, - d_model: int, - d_pair: int, - num_heads: int, - d_cond: int | None = None, - use_conditioning: bool = True, - ) -> None: - super().__init__() - self.d_model = d_model - self.num_heads = num_heads - self.head_dim = d_model // num_heads - self.scale = self.head_dim**-0.5 - d_cond = d_cond or d_model - - if use_conditioning: - self.adaln = AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) - self.out_gate = nn.Linear(d_cond, d_model, bias=True) - # adaln init: weight=0, bias=-2 - nn.init.zeros_(self.out_gate.weight) - nn.init.constant_(self.out_gate.bias, -2.0) - else: - self.pre_norm = nn.LayerNorm(d_model, eps=1e-5, dtype=torch.float32) - - self.q_proj = nn.Linear(d_model, d_model, bias=True) - self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) - self.g_proj = nn.Linear(d_model, d_model, bias=False) - self.out_proj = nn.Linear(d_model, d_model, bias=False) - - if d_pair > 0: - self.pair_norm = nn.LayerNorm(d_pair, eps=1e-5, dtype=torch.float32) - self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) - - def forward( - self, - a: Tensor, - s: Tensor | None, - z: Tensor, - attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - ) -> Tensor: - bsz, n_queries, d_model = a.shape - - if s is not None: - x = self.adaln(a, s) - else: - x = self.pre_norm(a.float()).to(a.dtype) - - n_keys = x.shape[1] - q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) - kv = self.kv_proj(x) - k, v = kv.chunk(2, dim=-1) - k = k.view(bsz, n_keys, self.num_heads, self.head_dim) - v = v.view(bsz, n_keys, self.num_heads, self.head_dim) - - # Expand z for num_diffusion_samples - if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: - z = z.repeat_interleave(num_diffusion_samples, dim=0) - if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: - attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) - - # Standard attention with pair bias - g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) - - logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale - - if z.dim() == 4: - pair_bias = self.pair_bias_proj(self.pair_norm(z.float()).to(z.dtype)) - else: - pair_bias = z.unsqueeze(-1) - logits = logits + pair_bias.to(dtype=logits.dtype) - - if attention_mask is not None: - min_val = torch.finfo(logits.dtype).min - mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) - logits = logits + mask_bias.to(dtype=logits.dtype) - - attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) - ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) - ctx = g * ctx - out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) - - if s is not None: - out = torch.sigmoid(self.out_gate(s)) * out - return out - - -# =========================================================================== -# ConditionedTransitionBlock -# =========================================================================== - - -class ConditionedTransitionBlock(nn.Module): - """Conditioned SwiGLU transition with adaptive layer norm.""" - - def __init__( - self, - d_model: int, - d_cond: int | None = None, - transition_multiplier: int = 2, - use_conditioning: bool = True, - ) -> None: - super().__init__() - d_cond = d_cond or d_model - hidden = transition_multiplier * d_model - - if use_conditioning: - self.adaln = AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) - self.output_gate = nn.Linear(d_cond, d_model, bias=True) - nn.init.zeros_(self.output_gate.weight) - nn.init.constant_(self.output_gate.bias, -2.0) - else: - self.pre_norm = nn.LayerNorm(d_model, eps=1e-5, dtype=torch.float32) - - self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) - self.lin_out = nn.Linear(hidden, d_model, bias=False) - - def forward(self, a: Tensor, s: Tensor | None) -> Tensor: - if s is not None: - x = self.adaln(a, s) - else: - x = self.pre_norm(a.float()).to(a.dtype) - - swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) - b = F.silu(swish_a) * swish_b - out = self.lin_out(b) - - if s is not None: - out = torch.sigmoid(self.output_gate(s)) * out - return out - - -# =========================================================================== -# DiffusionTransformer (token transformer) -# =========================================================================== - - -class DiffusionTransformer(nn.Module): - """Diffusion denoising transformer with attention pair bias.""" - - def __init__( - self, - d_model: int, - d_pair: int, - num_heads: int, - num_blocks: int, - d_cond: int | None = None, - transition_multiplier: int = 2, - use_conditioning: bool = True, - ) -> None: - super().__init__() - d_cond = d_cond or d_model - - self.attn_blocks = nn.ModuleList( - [ - AttentionPairBias( - d_model=d_model, - d_pair=d_pair, - num_heads=num_heads, - d_cond=d_cond, - use_conditioning=use_conditioning, - ) - for _ in range(num_blocks) - ] - ) - self.transition_blocks = nn.ModuleList( - [ - ConditionedTransitionBlock( - d_model=d_model, - d_cond=d_cond, - transition_multiplier=transition_multiplier, - use_conditioning=use_conditioning, - ) - for _ in range(num_blocks) - ] - ) - - def forward( - self, - a: Tensor, - s: Tensor | None, - z: Tensor, - attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - return_intermediates: bool = False, - ) -> tuple[Tensor, list[Tensor]]: - intermediates: list[Tensor] = [] - x = a - for attn, transition in zip(self.attn_blocks, self.transition_blocks): - x = x + attn( - x, - s, - z, - attention_mask=attention_mask, - num_diffusion_samples=num_diffusion_samples, - ) - x = x + transition(x, s) - if return_intermediates: - intermediates.append(x) - return x, intermediates - - -# =========================================================================== -# DiffusionConditioning -# =========================================================================== - - -class DiffusionConditioning(nn.Module): - """Conditions pair and single representations on noise timestep.""" - - def __init__( - self, - c_z: int = 256, - c_s: int = 768, - c_s_inputs: int = 451, - sigma_data: float = 16.0, - fourier_dim: int = 256, - transition_multiplier: int = 2, - layer_norm_eps: float = 1e-5, - ) -> None: - super().__init__() - self.sigma_data = float(sigma_data) - self.c_z = c_z - self.c_s = c_s - self.c_s_inputs = c_s_inputs - - self.z_input_norm = nn.LayerNorm(2 * c_z, eps=layer_norm_eps, dtype=torch.float32) - self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) - self.z_transitions = nn.ModuleList( - [TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] - ) - - self.s_input_norm = nn.LayerNorm(c_s_inputs, eps=layer_norm_eps, dtype=torch.float32) - self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) - self.fourier = FourierEmbedding(fourier_dim) - self.noise_norm = nn.LayerNorm(fourier_dim, eps=layer_norm_eps, dtype=torch.float32) - self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) - self.s_transitions = nn.ModuleList( - [TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] - ) - - def forward( - self, - t_hat: Tensor, - s_inputs: Tensor, - z_trunk: Tensor, - relative_position_encoding: Tensor, - sigma_data: float | None = None, - num_diffusion_samples: int = 1, - inference_cache: dict[str, Tensor] | None = None, - ) -> tuple[Tensor, Tensor]: - sigma = self.sigma_data if sigma_data is None else float(sigma_data) - base_batch = z_trunk.shape[0] - target_batch = base_batch * num_diffusion_samples - - # z conditioning (cached across diffusion steps — independent of t_hat) - if inference_cache is not None and "z" in inference_cache: - z = inference_cache["z"] - else: - z_rel = relative_position_encoding.to(dtype=torch.float32) - z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) - # The relpos/coords conditioning is fp32; z_input_norm keeps it fp32, - # then we hand off to z_proj in the model's compute dtype. - z = self.z_proj(self.z_input_norm(z).to(self.z_proj.weight.dtype)) - for block in self.z_transitions: - z = z + block(z) - if inference_cache is not None: - inference_cache["z"] = z - - # s conditioning - s_inputs_eff = s_inputs - if s_inputs_eff.shape[0] != target_batch: - s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) - - s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype)) - - # Noise embedding - t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) - if t.numel() == 1: - t = t.expand(target_batch) - elif t.shape[0] != target_batch: - t = t.repeat_interleave(num_diffusion_samples, 0) - t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) - n = self.fourier(t_noise) - n = self.noise_proj(self.noise_norm(n.float()).to(self.noise_proj.weight.dtype)) - s = s + n.unsqueeze(1) - - for block in self.s_transitions: - s = s + block(s) - - return s, z - - -# =========================================================================== -# DiffusionModule -# =========================================================================== - - -class DiffusionModule(nn.Module): - """Diffusion denoising module for structure prediction.""" - - def __init__( - self, - c_atom: int = 128, - c_token: int = 768, - c_z: int = 256, - c_s_inputs: int = 451, - sigma_data: float = 16.0, - fourier_dim: int = 256, - atom_num_blocks: int = 3, - atom_num_heads: int = 4, - token_num_blocks: int = 12, - token_num_heads: int = 16, - transition_multiplier: int = 2, - swa_window_size: int = 128, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.sigma_data = float(sigma_data) - - self.conditioning = DiffusionConditioning( - c_z=c_z, - c_s=c_token, # conditioning s output is c_token - c_s_inputs=c_s_inputs, - sigma_data=sigma_data, - fourier_dim=fourier_dim, - transition_multiplier=transition_multiplier, - ) - - # Atom encoder (structure_prediction=True, with coords_linear) - self.atom_encoder = ESMFold2AtomEncoder( - d_atom=c_atom, - d_token=c_token, - n_blocks=atom_num_blocks, - n_heads=atom_num_heads, - swa_window_size=swa_window_size, - expansion_ratio=2, - structure_prediction=True, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - # Atom decoder - self.atom_decoder = ESMFold2AtomDecoder( - d_atom=c_atom, - d_token=c_token, - n_blocks=atom_num_blocks, - n_heads=atom_num_heads, - swa_window_size=swa_window_size, - expansion_ratio=2, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - self.s_to_token = nn.Linear(c_token, c_token, bias=False) - nn.init.zeros_(self.s_to_token.weight) - - # Token transformer (DiffusionTransformer with pair bias) - self.token_transformer = DiffusionTransformer( - d_model=c_token, - d_pair=c_z, - num_heads=token_num_heads, - num_blocks=token_num_blocks, - d_cond=c_token, - transition_multiplier=transition_multiplier, - use_conditioning=True, - ) - - self.s_step_norm = nn.LayerNorm(c_token, dtype=torch.float32) - self.token_norm = nn.LayerNorm(c_token, dtype=torch.float32) - - def forward( - self, - x_noisy: Tensor, - t_hat: Tensor, - ref_pos: Tensor, - ref_charge: Tensor, - ref_mask: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - tok_idx: Tensor, - s_inputs: Tensor, - z_trunk: Tensor, - relative_position_encoding: Tensor, - asym_id: Tensor, - residue_index: Tensor, - entity_id: Tensor, - token_index: Tensor, - sym_id: Tensor, - sigma_data: float | None = None, - token_attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - return_atom_repr: bool = False, - inference_cache: dict[str, Tensor] | None = None, - ) -> dict[str, Tensor | None]: - bsz = x_noisy.shape[0] - sigma = self.sigma_data if sigma_data is None else float(sigma_data) - t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape(-1) - if t.numel() == 1: - t = t.expand(bsz) - - # Step 1: conditioning (pair z is cached across diffusion steps) - s, z = self.conditioning( - t_hat=t, - s_inputs=s_inputs, - z_trunk=z_trunk, - relative_position_encoding=relative_position_encoding, - sigma_data=sigma, - num_diffusion_samples=num_diffusion_samples, - inference_cache=inference_cache, - ) - - # Step 2: normalize noisy coords - denom = torch.sqrt(t * t + sigma * sigma) - r_noisy = x_noisy / denom[:, None, None] - - # Step 3: atom encoder - a, q_skip, c_skip, p_skip, enc_intermediates = self.atom_encoder( - ref_pos=ref_pos, - atom_attention_mask=ref_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=tok_idx, - r_l=r_noisy, - num_diffusion_samples=num_diffusion_samples, - return_intermediates=return_atom_repr, - inference_cache=inference_cache, - ) - - # Step 4: add conditioned s - a = a + self.s_to_token(self.s_step_norm(s.float()).to(s.dtype)) - - # Step 5: token transformer - a, _ = self.token_transformer( - a, - s, - z, - attention_mask=token_attention_mask, - num_diffusion_samples=num_diffusion_samples, - ) - - # Step 6: token norm - a = self.token_norm(a.float()).to(a.dtype) - - # Step 7: atom decoder - r_update, dec_intermediates = self.atom_decoder( - a_i=a, - q_l=q_skip, - c_l=c_skip, - p_lm=p_skip, - atom_to_token=tok_idx, - atom_attention_mask=ref_mask, - num_diffusion_samples=num_diffusion_samples, - return_intermediates=return_atom_repr, - ) - - # Step 8: compute denoised output - sigma2 = sigma * sigma - t2 = t * t - out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy - out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update - - # Collect atom intermediates from encoder + decoder - atom_intermediates: Tensor | None = None - if return_atom_repr: - all_ints = enc_intermediates + dec_intermediates - if all_ints: - atom_intermediates = torch.stack(all_ints, dim=2) - - return { - "x_denoised": out, - "atom_intermediates": atom_intermediates, - } - - -# =========================================================================== -# DiffusionStructureHead -# =========================================================================== - - -class DiffusionStructureHead(nn.Module): - """Wrapper around DiffusionModule with diffusion sampling.""" - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__() - dm = config.structure_head.diffusion_module - swa_cfg = config.inputs.atom_encoder - sh = config.structure_head - - self.diffusion_module = DiffusionModule( - c_atom=dm.c_atom, - c_token=dm.c_token, - c_z=dm.c_z, - c_s_inputs=dm.c_s_inputs, - sigma_data=dm.sigma_data, - fourier_dim=dm.fourier_dim, - atom_num_blocks=dm.atom_num_blocks, - atom_num_heads=dm.atom_num_heads, - token_num_blocks=dm.token_num_blocks, - token_num_heads=dm.token_num_heads, - transition_multiplier=dm.transition_multiplier, - swa_window_size=swa_cfg.swa_window_size, - spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, - uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, - ) - - # Sampling hyperparameters - self.sigma_data = dm.sigma_data - self.gamma_0 = sh.gamma_0 - self.gamma_min = sh.gamma_min - self.noise_scale = sh.noise_scale - self.step_scale = sh.step_scale - self.inference_s_max = sh.inference_s_max - self.inference_s_min = sh.inference_s_min - self.inference_p = sh.inference_p - self.inference_num_steps = sh.inference_num_steps - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def inference_noise_schedule(self, num_steps: int | None = None, device: torch.device | None = None) -> Tensor: - """Karras power-law noise schedule.""" - steps = self.inference_num_steps if num_steps is None else int(num_steps) - if steps == 1: - return torch.tensor( - [self.inference_s_max * self.sigma_data, 0.0], - device=device, - dtype=torch.float32, - ) - p = float(self.inference_p) - inv_p = 1.0 / p - k = torch.arange(steps, device=device, dtype=torch.float32) - base = self.inference_s_max**inv_p + (k / (steps - 1)) * ( - self.inference_s_min**inv_p - self.inference_s_max**inv_p - ) - schedule = self.sigma_data * base.pow(p) - return F.pad(schedule, (0, 1), value=0.0) - - @staticmethod - def _random_rotations(n: int, dtype: torch.dtype, device: torch.device) -> Tensor: - q = torch.randn((n, 4), dtype=dtype, device=device) - scale = torch.sqrt((q * q).sum(dim=1)) - signs = torch.where(q[:, 0] < 0, -scale, scale) - q = q / signs[:, None] - r, i, j, k = torch.unbind(q, dim=-1) - two_s = 2.0 / (q * q).sum(dim=-1) - return torch.stack( - ( - 1 - two_s * (j * j + k * k), - two_s * (i * j - k * r), - two_s * (i * k + j * r), - two_s * (i * j + k * r), - 1 - two_s * (i * i + k * k), - two_s * (j * k - i * r), - two_s * (i * k - j * r), - two_s * (j * k + i * r), - 1 - two_s * (i * i + j * j), - ), - dim=-1, - ).reshape(n, 3, 3) - - def _center_random_augmentation( - self, x: Tensor, atom_mask: Tensor, second_coords: Tensor | None = None - ) -> tuple[Tensor, Tensor | None]: - """Algorithm 19: center + random rotation + translation.""" - bsz = x.shape[0] - mask = atom_mask.unsqueeze(-1) # [B, A, 1] - denom = mask.sum(dim=1, keepdim=True).clamp(min=1) - mean = (x * mask).sum(dim=1, keepdim=True) / denom - x = x - mean - if second_coords is not None: - second_coords = second_coords - mean - - r = self._random_rotations(bsz, x.dtype, x.device) - x = torch.einsum("bmd,bds->bms", x, r) - if second_coords is not None: - second_coords = torch.einsum("bmd,bds->bms", second_coords, r) - - t = torch.randn_like(x[:, 0:1, :]) - x = x + t - if second_coords is not None: - second_coords = second_coords + t - return x, second_coords - - @staticmethod - def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> Tensor: - """Kabsch alignment: align x to x_gt with weights w.""" - w = (mask * w).unsqueeze(-1) # [B, N, 1] - denom = w.sum(dim=-2, keepdim=True).clamp(min=1e-8) - mu = (x * w).sum(dim=-2, keepdim=True) / denom - mu_gt = (x_gt * w).sum(dim=-2, keepdim=True) / denom - x_c = x - mu - xgt_c = x_gt - mu_gt - H = torch.einsum("bni,bnj->bij", w * xgt_c, x_c) - H32 = H.float() - U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) - det = torch.linalg.det(U @ Vh) - ones = torch.ones_like(det) - R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to(H.dtype) - return x_c @ R.transpose(-1, -2) + mu_gt - - # ------------------------------------------------------------------ - # Sampling - # ------------------------------------------------------------------ - - @torch.inference_mode() - def sample( - self, - z_trunk: Tensor, - s_inputs: Tensor, - relative_position_encoding: Tensor, - ref_pos: Tensor, - ref_charge: Tensor, - ref_mask: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - tok_idx: Tensor, - asym_id: Tensor, - residue_index: Tensor, - entity_id: Tensor, - token_index: Tensor, - sym_id: Tensor, - token_attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - num_sampling_steps: int | None = None, - max_inference_sigma: float | None = 256.0, - noise_scale: float | None = None, - step_scale: float | None = None, - return_atom_repr: bool = False, - use_inference_cache: bool = True, - denoising_early_exit_rmsd: float | None = None, - ) -> dict[str, Tensor | None]: - """Diffusion sampling (Algorithm 18). - - ``num_sampling_steps`` is the number of denoising steps actually run. - When ``max_inference_sigma`` is set, the Karras schedule built with - ``num_sampling_steps`` entries would lose its high-σ tail to the cap, - so we inflate the underlying schedule length here to land back at the - requested step count post-truncation. - """ - n_atoms = tok_idx.shape[1] - device = s_inputs.device - target_batch = s_inputs.shape[0] * num_diffusion_samples - - inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None - - steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) - - schedule = self.inference_noise_schedule(steps, device) - if max_inference_sigma is not None: - schedule = schedule[schedule <= float(max_inference_sigma)] - schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) - - lam = self.noise_scale if noise_scale is None else float(noise_scale) - eta = self.step_scale if step_scale is None else float(step_scale) - - x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) - atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() - - gammas = torch.where( - schedule > self.gamma_min, - torch.full_like(schedule, self.gamma_0), - torch.zeros_like(schedule), - ) - - x_denoised_prev: Tensor | None = None - diff_atom_intermediates: Tensor | None = None - - step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) - num_steps = len(step_pairs) - - for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): - x, x_denoised_prev = self._center_random_augmentation(x, atom_mask, second_coords=x_denoised_prev) - - sigma_tm_val = float(sigma_tm.item()) - t_hat_val = sigma_tm_val * (1.0 + float(gamma.item())) - eps_std = lam * max(t_hat_val**2 - sigma_tm_val**2, 0.0) ** 0.5 - x_noisy = x + eps_std * torch.randn_like(x) - - is_last_step = step_idx == num_steps - 1 - request_atom_repr = return_atom_repr and (is_last_step or denoising_early_exit_rmsd is not None) - - dm_out = self.diffusion_module( - x_noisy=x_noisy, - t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), - ref_pos=ref_pos, - ref_charge=ref_charge, - ref_mask=ref_mask, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - ref_space_uid=ref_space_uid, - tok_idx=tok_idx, - s_inputs=s_inputs, - z_trunk=z_trunk, - relative_position_encoding=relative_position_encoding, - asym_id=asym_id, - residue_index=residue_index, - entity_id=entity_id, - token_index=token_index, - sym_id=sym_id, - token_attention_mask=token_attention_mask, - num_diffusion_samples=num_diffusion_samples, - return_atom_repr=request_atom_repr, - inference_cache=inference_cache, - ) - - x_denoised = dm_out["x_denoised"] - if request_atom_repr: - diff_atom_intermediates = dm_out.get("atom_intermediates") - - # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts - # to fp32 internally for the SVD/det. - x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) - x_noisy = x_noisy.to(dtype=x_denoised.dtype) - - # ODE/SDE step - sigma_t_val = float(sigma_t.item()) - denoised_over_sigma = (x_noisy - x_denoised) / t_hat_val - x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma - - # Denoising early-exit: stop when consecutive predictions converge - if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: - aligned = self._weighted_rigid_align( - x_denoised_prev.float(), - x_denoised.float(), - atom_mask, - atom_mask, - ) - diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) - per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() - if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: - x = x_denoised - x_denoised_prev = x_denoised - break - - x_denoised_prev = x_denoised - - result: dict[str, Tensor | None] = { - "sample_atom_coords": x, - } - if return_atom_repr: - result["diff_atom_intermediates"] = diff_atom_intermediates - return result - - -# =========================================================================== -# RowAttentionPooling -# =========================================================================== - - -class RowAttentionPooling(nn.Module): - """Row-wise attention pooling: attn_proj, out_proj.""" - - def __init__(self, d_pair: int, d_single: int) -> None: - super().__init__() - self.attn_proj = nn.Linear(d_pair, 1, bias=False) - self.out_proj = nn.Linear(d_pair, d_single, bias=False) - - def forward(self, z: Tensor, mask: Tensor) -> Tensor: - scores = self.attn_proj(z).squeeze(-1) - mask_bias = torch.where( - mask[:, None, :].bool(), - torch.zeros_like(scores), - torch.full_like(scores, -1e9), - ) - scores = scores + mask_bias - weights = F.softmax(scores, dim=-1, dtype=torch.float32).to(scores.dtype) - pooled = torch.einsum("bnm,bnmd->bnd", weights, z) - return self.out_proj(pooled) - - -# =========================================================================== -# InputsEmbedder -# =========================================================================== - - -class InputsEmbedder(nn.Module): - """Embeds input features including atom-level encoding via SWA attention.""" - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__() - swa_cfg = config.inputs.atom_encoder - - self.atom_attention_encoder = ESMFold2AtomEncoder( - d_atom=swa_cfg.d_atom, - d_token=swa_cfg.d_token, - n_blocks=swa_cfg.n_blocks, - n_heads=swa_cfg.n_heads, - swa_window_size=swa_cfg.swa_window_size, - expansion_ratio=swa_cfg.expansion_ratio, - structure_prediction=False, # no coords_linear - spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, - uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, - ) - - def forward( - self, - aatype: Tensor, - profile: Tensor, - deletion_mean: Tensor, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, - ) -> Tensor: - """Embed inputs into per-token features. - - Returns: - [B, L, d_inputs] concatenation of atom encoding, aatype, profile, - and deletion_mean. - """ - a, _q, _c, _attn_params, _intermediates = self.atom_attention_encoder( - ref_pos=ref_pos, - atom_attention_mask=atom_attention_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=atom_to_token, - ) - # The continuous input features are fp32; fold them into the atom - # encoding's (compute) dtype so the single representation is one dtype. - dtype = a.dtype - return torch.cat( - [a, aatype.to(dtype), profile.to(dtype), deletion_mean.unsqueeze(-1).to(dtype)], - dim=-1, - ) - - -# =========================================================================== -# ResIdxAsymIdSymIdEntityIdEncoding (trunk relative position) -# =========================================================================== - - -class ResIdxAsymIdSymIdEntityIdEncoding(nn.Module): - """embed.weight [d_pair, n_features] where n_features = 2*(2*r_bins+2) + 1 + (2*c_bins+2). - - For default r_bins=32, c_bins=2: 2*66 + 1 + 6 = 139. - """ - - def __init__( - self, - n_relative_residx_bins: int = 32, - n_relative_chain_bins: int = 2, - d_pair: int = 256, - ) -> None: - super().__init__() - self.n_relative_residx_bins = n_relative_residx_bins - self.n_relative_chain_bins = n_relative_chain_bins - self.d_pair = d_pair - - n_feats_residue = 2 * n_relative_residx_bins + 2 - n_feats_token = 2 * n_relative_residx_bins + 2 - n_feats_chain = 2 * n_relative_chain_bins + 2 - n_feats_same_entity = 1 - total_feats = n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity - self.embed = nn.Linear(total_feats, d_pair, bias=False) - - def forward( - self, - residue_index: Tensor, - asym_id: Tensor, - sym_id: Tensor, - entity_id: Tensor, - token_index: Tensor, - ) -> Tensor: - bij_same_chain = asym_id.unsqueeze(2) == asym_id.unsqueeze(1) - bij_same_residue = residue_index.unsqueeze(2) == residue_index.unsqueeze(1) - bij_same_entity = entity_id.unsqueeze(2) == entity_id.unsqueeze(1) - - dij_residue = residue_index.unsqueeze(2) - residue_index.unsqueeze(1) - dij_residue = torch.clip( - dij_residue + self.n_relative_residx_bins, - 0, - 2 * self.n_relative_residx_bins, - ) - dij_residue = torch.where(bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1) - aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) - - dij_token = torch.clip( - token_index.unsqueeze(2) - token_index.unsqueeze(1) + self.n_relative_residx_bins, - 0, - 2 * self.n_relative_residx_bins, - ) - dij_token = torch.where( - bij_same_chain & bij_same_residue, - dij_token, - 2 * self.n_relative_residx_bins + 1, - ) - aij_rel_token = F.one_hot(dij_token, 2 * self.n_relative_residx_bins + 2) - - dij_chain = torch.clip( - sym_id.unsqueeze(2) - sym_id.unsqueeze(1) + self.n_relative_chain_bins, - 0, - 2 * self.n_relative_chain_bins, - ) - dij_chain = torch.where(bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain) - aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) - - feats = torch.cat( - [ - aij_rel_pos.float(), - aij_rel_token.float(), - bij_same_entity.float().unsqueeze(-1), - aij_rel_chain.float(), - ], - dim=-1, - ) - - return self.embed(feats.to(self.embed.weight.dtype)) - - -# =========================================================================== -# SingleToPair (for LanguageModelShim) -# =========================================================================== - - -class SingleToPair(nn.Module): - """downproject, output_mlp (Sequential of Linear, GELU, Linear).""" - - def __init__(self, input_dim: int, downproject_dim: int, output_dim: int) -> None: - super().__init__() - self.downproject = nn.Linear(input_dim, downproject_dim) - self.output_mlp = nn.Sequential( - nn.Linear(2 * downproject_dim, output_dim), - nn.GELU(), - nn.Linear(output_dim, output_dim), - ) - - def forward(self, x: Tensor) -> Tensor: - x = self.downproject(x) - x = torch.cat( - [(x.unsqueeze(2) * x.unsqueeze(1)), (x.unsqueeze(2) - x.unsqueeze(1))], - dim=3, - ) - return self.output_mlp(x) - - -# =========================================================================== -# LanguageModelShim -# =========================================================================== - - -class LanguageModelShim(nn.Module): - """Shim holding the trainable projection weights for LM integration. - - Contains: - - base_z_combine: nn.Parameter [num_layers+1] - - base_z_linear: Sequential(nn.LayerNorm(d_model), Linear(d_model, d_z, bias=False)) - - base_z_mlp: Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z)) - """ - - def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: - super().__init__() - - self.base_z_mlp = nn.Sequential(SingleToPair(d_z, d_z, d_z), nn.LayerNorm(d_z, dtype=torch.float32)) - self.base_z_linear = nn.Sequential( - nn.LayerNorm(d_model, dtype=torch.float32), nn.Linear(d_model, d_z, bias=False) - ) - self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) - - def forward(self, hidden_states: Tensor) -> Tensor: - """Project pre-computed ESMC hidden states to pair representation. - - Args: - hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. - - Returns: - [B, L, L, d_pair] pair representation. - """ - # The ESMC backbone may be loaded at a different precision than the trunk - # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. - hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) - # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. - normed = self.base_z_linear[0](hidden_states.float()).to(hidden_states.dtype) - lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] - weights = self.base_z_combine.softmax(0) # [81] - lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] - # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. - pair = self.base_z_mlp[0](lm_z) - lm_z = self.base_z_mlp[1](pair.float()).to(pair.dtype) # [B, L, L, d_z] - return lm_z - - -# =========================================================================== -# ESMFold2 — language-model backbone helpers -# =========================================================================== - - -def compute_lm_hidden_states( - esmc: nn.Module, - input_ids: Tensor, - asym_id: Tensor, - residue_index: Tensor, - mol_type: Tensor, - token_mask: Tensor, - pad_to_multiple: int | None = None, -) -> Tensor: - """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. - - Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple - structure tokens but share a single ``(asym_id, residue_index)`` key — - collapse them to one LM token per residue before running the LM (the LM - was trained on per-residue inputs, not per-atom), then scatter the - hidden states back to the per-token layout. - """ - B, L = input_ids.shape - device = input_ids.device - protein_mask = (mol_type == 0) & token_mask - - lm_input_list = [] - lm_lengths = [] - # Per-batch maps from (original protein-token index) to (LM input position). - expand_maps: list[Tensor] = [] - for b in range(B): - mask_b = protein_mask[b] - ids_b = input_ids[b][mask_b] - asym_b = asym_id[b][mask_b] - res_b = residue_index[b][mask_b] - - # Collapse: keep first token per (asym_id, residue_index) key, in - # input order. ``inverse`` maps each original protein-token to its - # collapsed residue index. - keys = torch.stack((asym_b, res_b), dim=1) - unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) - n_unique = unique_keys.size(0) - token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) - first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) - first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) - ordered = torch.argsort(first_pos) - first_pos_ordered = first_pos[ordered] - ids_collapsed = ids_b[first_pos_ordered] - asym_collapsed = asym_b[first_pos_ordered] - remap = torch.empty_like(ordered) - remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) - inverse_ordered = remap[inverse] - - chain_ids = asym_collapsed.unique(sorted=True) - # [BOS] chain1 [EOS BOS] chain2 ... [EOS] - parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] - # Per-chain LM positions accumulate; track them for the expand map. - per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) - cursor = 1 # position 0 is the leading BOS - for i, cid in enumerate(chain_ids): - in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] - parts.append(ids_collapsed[in_chain]) - per_token_lm_pos[in_chain] = torch.arange( - cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long - ) - cursor += in_chain.shape[0] - if i < len(chain_ids) - 1: - parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) - cursor += 2 # EOS + BOS - parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) - lm_seq = torch.cat(parts) - lm_input_list.append(lm_seq) - lm_lengths.append(lm_seq.shape[0]) - - # Original protein-token position → LM input position. - prot_pos_b = mask_b.nonzero(as_tuple=True)[0] - expand_map = torch.full((L,), -1, device=device, dtype=torch.long) - expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] - expand_maps.append(expand_map) - - # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. - max_len = max(lm_lengths) - if pad_to_multiple is not None and pad_to_multiple > 1: - max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple - lm_input_ids = torch.full( - (B, max_len), - 1, - device=device, - dtype=input_ids.dtype, # PAD=1 - ) - for b in range(B): - lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] - - # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). - sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 - sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 - - with torch.inference_mode(): - esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) - - hs = esmc_out.hidden_states # [n_layers+1, B, max_len, D] - n_layers_plus_1, _, _, D = hs.shape - result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) - for b in range(B): - mb = protein_mask[b] - em = expand_maps[b][mb] # [n_protein_tokens] LM positions - # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] - gathered = hs[:, b, em, :].permute(1, 0, 2) - result[b, mb.nonzero(as_tuple=True)[0]] = gathered - - return result.detach() - - -# =========================================================================== -# TriangleMultiplicativeUpdate -# =========================================================================== -@use_kernel_forward_from_hub("ESMFold2TriangleMultiplication") -class TriangleMultiplicativeBlock(nn.Module): - """Triangle multiplicative update block with gated signal routing. - - The O(N^3) triangular contraction below is the trunk's dominant cost. Loading - with ``ESMFold2Model.from_pretrained(..., device_map="cuda", use_kernels=True)`` - (CUDA + inference) swaps the whole block forward for a fused Triton kernel from - the Hub (see the ``hub_kernels`` mapping); the pure-PyTorch ``forward`` here stays - as the reference/fallback. The kernel reads this module's parameters - (``norm_start``/``norm_mix``/``proj_bundle``/``proj_emit``/``proj_gate``) and - matches ``forward``'s ``(pair_grid, visibility)`` signature, returning the - residual-free delta. - """ - - _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} - _VALID_FLOWS = ("outgoing", "incoming") - - def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None: - super().__init__() - if flow not in self._FLOW_TO_EINSUM: - raise ValueError(f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}.") - - self.input_channels = input_channels - self.latent_channels = latent_channels - self.flow = flow - self._einsum_equation = self._FLOW_TO_EINSUM[flow] - self.norm_start = nn.LayerNorm(self.input_channels, eps=_EPS, dtype=torch.float32) - self.norm_mix = nn.LayerNorm(self.latent_channels, eps=_EPS, dtype=torch.float32) - self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) - self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) - self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) - - # Default chunked for memory on long sequences; tests override with - # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 - # parity checks. - self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: - return torch.einsum(self._einsum_equation, left_stream, right_stream) - - def _triangular_contract_chunked(self, left_stream: Tensor, right_stream: Tensor, chunk_size: int) -> Tensor: - """Compute the triangular einsum in chunks along the output i-dimension.""" - L = left_stream.shape[1] if self.flow == "outgoing" else left_stream.shape[2] - chunks = [] - for start in range(0, L, chunk_size): - end = min(start + chunk_size, L) - if self.flow == "outgoing": - chunk = torch.einsum(self._einsum_equation, left_stream[:, start:end], right_stream) - else: - chunk = torch.einsum(self._einsum_equation, left_stream[:, :, start:end], right_stream) - chunks.append(chunk) - return torch.cat(chunks, dim=1) - - def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor: - if visibility is None: - visibility = pair_grid.new_ones(pair_grid.shape[:-1]) - - normalized_grid = self.norm_start(pair_grid.float()).to(pair_grid.dtype) - bundled = self.proj_bundle(normalized_grid) - signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) - # Gates and the O(N^3) contraction run in the activation dtype (bf16). This - # matches the reference: under its autocast the einsum is downcast to bf16, - # and the fused Triton kernel likewise contracts in bf16 — the dtype the - # checkpoint was trained with. Keeping these in fp32 was a (marginal) - # precision *up* that diverges from training and is slower on the trunk's - # dominant op. ``norm_start``/``norm_mix`` stay fp32. A no-op in fp32. - routed = signal * torch.sigmoid(gate_logits) - routed = routed * visibility.unsqueeze(-1) - - left_stream, right_stream = routed.chunk(2, dim=-1) - if self._chunk_size is not None: - contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) - else: - contracted = self._triangular_contract(left_stream, right_stream) - mixed = self.proj_emit(self.norm_mix(contracted.float()).to(self.proj_emit.weight.dtype)) - output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) - return mixed * output_gate - - -class TriangleMultiplicativeUpdate(nn.Module): - """Thin wrapper exposing the triangular mixer with explicit orientation (v3).""" - - def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: - super().__init__() - flow = "outgoing" if _outgoing else "incoming" - self._engine = TriangleMultiplicativeBlock(input_channels=dim, latent_channels=dim, flow=flow) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._engine.set_chunk_size(chunk_size) - - def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: - return self._engine(z, visibility=mask) - - -# =========================================================================== -# FoldingTrunk: Transition, PairUpdateBlock, FoldingTrunk -# =========================================================================== - - -class Transition(nn.Module): - """LayerNorm + SwiGLU feed-forward residual block, chunked along the token axis.""" - - def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: - super().__init__() - self.norm = nn.LayerNorm(d_model, dtype=torch.float32) - self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) - # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. - self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def forward(self, x: Tensor) -> Tensor: - if self._chunk_size is None or x.shape[1] <= self._chunk_size: - return x + self.ffn(self.norm(x.float()).to(x.dtype)) - out_list: list[Tensor] = [] - for s in range(0, x.shape[1], self._chunk_size): - e = min(s + self._chunk_size, x.shape[1]) - sl = x[:, s:e] - out_list.append(sl + self.ffn(self.norm(sl.float()).to(sl.dtype))) - return torch.cat(out_list, dim=1) - - -class PairUpdateBlock(nn.Module): - """tri_mul_out, tri_mul_in, pair_transition.""" - - def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: - super().__init__() - self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) - self.pair_transition = Transition(d_pair, expansion_ratio=expansion_ratio) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self.tri_mul_out.set_chunk_size(chunk_size) - self.tri_mul_in.set_chunk_size(chunk_size) - self.pair_transition.set_chunk_size(chunk_size) - - def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: - # HF model is inference-only, so the trained row-shared dropout (r=0) is a no-op. - pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) - pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) - pair = self.pair_transition(pair) - return pair - - -class FoldingTrunk(nn.Module): - """ModuleList of PairUpdateBlocks.""" - - def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4) -> None: - super().__init__() - self.blocks = nn.ModuleList( - [PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) for _ in range(n_layers)] - ) - - def set_chunk_size(self, chunk_size: int | None) -> None: - for block in self.blocks: - block.set_chunk_size(chunk_size) - - def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: - for block in self.blocks: - fn = partial(block, pair_attention_mask=pair_attention_mask) - if torch.is_grad_enabled(): - pair = checkpoint(fn, pair, use_reentrant=False) - else: - pair = fn(pair) - return pair - - -# =========================================================================== -# MSA Encoder -# =========================================================================== - - -class OuterProductMean(nn.Module): - """Outer-product mean: maps an MSA representation into a pair update. - - The order of the ``/ n_valid`` divide vs. the ``Wout`` projection is - selectable via ``divide_outer_before_proj`` because different ESMFold2 - checkpoints were trained with different orderings: - - * ``False`` (default): ``Wout(outer) / n_valid`` — the projection bias - is scaled by 1/n_valid alongside the outer product. - * ``True``: ``Wout(outer / n_valid)`` — the projection bias is added - unscaled, post-divide. - """ - - def __init__( - self, - d_msa: int, - d_hidden: int, - d_pair: int, - divide_outer_before_proj: bool = False, - ) -> None: - super().__init__() - self.d_hidden = d_hidden - self.divide_outer_before_proj = divide_outer_before_proj - self.norm = nn.LayerNorm(d_msa, dtype=torch.float32) - self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) - self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) - # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. - self._chunk_size: int | None = None - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: - m_norm = self.norm(m.float()).to(m.dtype) - x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) - a, b = x.chunk(2, dim=-1) - mask_f = msa_attention_mask.to(a.dtype) - n_valid = (mask_f @ mask_f.transpose(-1, -2)).unsqueeze(-1).clamp(min=1.0) - if self._chunk_size is None: - outer = torch.einsum("bimc,bjmd->bijcd", a, b).flatten(-2) - if self.divide_outer_before_proj: - return self.Wout(outer / n_valid) - return self.Wout(outer) / n_valid - # Chunk along the left (i) axis so the peak einsum intermediate is - # [B, chunk, L, c, d] instead of [B, L, L, c, d]. - L = a.shape[1] - out_chunks: list[Tensor] = [] - for s in range(0, L, self._chunk_size): - e = min(s + self._chunk_size, L) - outer_chunk = torch.einsum("bimc,bjmd->bijcd", a[:, s:e], b).flatten(-2) - if self.divide_outer_before_proj: - out_chunks.append(self.Wout(outer_chunk / n_valid[:, s:e])) - else: - out_chunks.append(self.Wout(outer_chunk) / n_valid[:, s:e]) - return torch.cat(out_chunks, dim=1) - - -class MSAPairWeightedAveraging(nn.Module): - """Pair-biased MSA row update (AF3 Supplement Algorithm 10).""" - - def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32) -> None: - super().__init__() - self.n_heads = n_heads - self.head_width = head_width - self.norm_single = nn.LayerNorm(d_msa, dtype=torch.float32) - self.compute_bias = nn.Sequential( - nn.LayerNorm(d_pair, dtype=torch.float32), nn.Linear(d_pair, n_heads, bias=False) - ) - self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) - self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) - self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) - - def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor) -> Tensor: - """ - Args: - msa_repr: [B, L, M, d_msa] - pair_repr: [B, L, L, d_pair] - pair_attention_mask:[B, L, L] - Returns: - [B, L, M, d_msa] - """ - B, L, M, _ = msa_repr.shape - h, dh = self.n_heads, self.head_width - - msa_normed = self.norm_single(msa_repr.float()).to(msa_repr.dtype) - bias = self.compute_bias[1](self.compute_bias[0](pair_repr.float()).to(pair_repr.dtype)) # [B, L, L, n_heads] - bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) - attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j - - v = self.Wv(msa_normed).reshape(B, L, M, h, dh) - gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) - - output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) - return self.Wout(output.reshape(B, L, M, h * dh)) diff --git a/src/transformers/models/esmfold2/protein_utils.py b/src/transformers/models/esmfold2/protein_utils.py index 5fb332907fad..41609e808bfe 100644 --- a/src/transformers/models/esmfold2/protein_utils.py +++ b/src/transformers/models/esmfold2/protein_utils.py @@ -483,28 +483,16 @@ def prepare_protein_features(sequence: str) -> dict[str, Tensor]: _RES_TYPE_TO_3LETTER: dict[int, str] = {rt: three for three, rt in PROTEIN_RESIDUE_TO_RES_TYPE.items()} _RES_TYPE_TO_3LETTER[PROTEIN_UNK_RES_TYPE] = "UNK" -# Featurization keys that ``output_to_pdb`` reads off the forward output. -# ``infer_protein`` re-attaches them because ``forward`` does not echo them -# back; both ESMFold2 model classes share this list. -OUTPUT_TO_PDB_FEATURE_KEYS: tuple[str, ...] = ( - "res_type", - "atom_to_token", - "ref_atom_name_chars", - "atom_attention_mask", - "token_attention_mask", - "residue_index", -) - - -def output_to_pdb(output: dict) -> str: - """Convert an ESMFold2 protein forward output into a PDB string. - - Expects ``output`` to carry the featurization keys re-attached by - ``infer_protein`` (``res_type``, ``atom_to_token``, - ``ref_atom_name_chars``, ``atom_attention_mask``, - ``token_attention_mask``, ``residue_index``) alongside the predicted - ``sample_atom_coords`` and ``plddt``. Builds a 37-atom - ``OFProtein`` (per-atom pLDDT in the b-factor column) and renders it + +def output_to_pdb(output, features: dict) -> str: + """Convert an ESMFold2 forward output into a PDB string. + + Reads the predicted ``sample_atom_coords`` and ``plddt`` from ``output`` (an + [`~transformers.models.esmfold2.modeling_esmfold2.ESMFold2Output`]) and the + featurization tensors it needs (``res_type``, ``atom_to_token``, + ``ref_atom_name_chars``, ``atom_attention_mask``, ``token_attention_mask``, + ``residue_index``) from the ``features`` dict the model was run on. Builds a + 37-atom ``OFProtein`` (per-atom pLDDT in the b-factor column) and renders it with the OpenFold utilities shipped in ``transformers.models.esm``. """ from transformers.models.esm.openfold_utils import OFProtein, to_pdb @@ -516,12 +504,12 @@ def output_to_pdb(output: dict) -> str: coords = coords.detach().cpu().numpy()[0] plddt = output["plddt"].detach().cpu().numpy()[0] - atom_to_token = output["atom_to_token"].cpu().numpy() - ref_chars = output["ref_atom_name_chars"].cpu().numpy() - res_type = output["res_type"].cpu().numpy() - token_mask = output["token_attention_mask"].cpu().numpy().astype(bool) - atom_mask_in = output["atom_attention_mask"].cpu().numpy().astype(bool) - residue_index_arr = output["residue_index"].cpu().numpy() + atom_to_token = features["atom_to_token"].cpu().numpy() + ref_chars = features["ref_atom_name_chars"].cpu().numpy() + res_type = features["res_type"].cpu().numpy() + token_mask = features["token_attention_mask"].cpu().numpy().astype(bool) + atom_mask_in = features["atom_attention_mask"].cpu().numpy().astype(bool) + residue_index_arr = features["residue_index"].cpu().numpy() if atom_to_token.ndim == 2: atom_to_token = atom_to_token[0] diff --git a/tests/models/esmc/test_modeling_esmc.py b/tests/models/esmc/test_modeling_esmc.py index 350af3bbe085..83b60890d5a6 100644 --- a/tests/models/esmc/test_modeling_esmc.py +++ b/tests/models/esmc/test_modeling_esmc.py @@ -138,7 +138,7 @@ def prepare_config_and_inputs_for_common(self): @require_torch class ESMCModelTest(ModelTesterMixin, PipelineTesterMixin, unittest.TestCase): test_mismatched_shapes = False - test_resize_embeddings = False # ESMC's lm_head is an nn.Sequential, not a tied decoder + test_resize_embeddings = False # ESMC's lm_head decoder is untied (tie_word_embeddings=False) all_model_classes = ( ( diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index c24757d16ac0..8f97600d11b4 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -14,8 +14,8 @@ """Testing suite for the PyTorch ESMFold2 model. ESMFold2 is an all-atom structure predictor: its forward takes ~18 structural -feature tensors (built from a sequence by ``prepare_protein_features``) and -returns a plain ``dict`` rather than a ``ModelOutput``, so it does not plug into +feature tensors (built from a sequence by ``prepare_protein_features``) rather +than the standard ``input_ids``/``attention_mask``, so it does not plug into ``ModelTesterMixin`` (the file is registered in ``utils/check_repo.py::TEST_FILES_WITH_NO_COMMON_TESTS``). Coverage here is the config (round-trip / nesting), a CPU forward smoke test across attention @@ -41,7 +41,7 @@ import torch from transformers import ESMFold2Model - from transformers.models.esmfold2.modeling_esmfold2_common import SWA3DRoPEAttention + from transformers.models.esmfold2.modeling_esmfold2 import ESMFold2SWA3DRoPEAttention def get_tiny_config(**overrides) -> "ESMFold2Config": @@ -57,8 +57,7 @@ def get_tiny_config(**overrides) -> "ESMFold2Config": "d_pair": 16, "num_loops": 1, "num_diffusion_samples": 1, - "lm_d_model": 32, - "lm_num_layers": 1, + "esmc_config": {"d_model": 32, "n_heads": 2, "n_layers": 1, "vocab_size": 64}, "inputs": { "d_inputs": 83, "atom_encoder": { @@ -150,12 +149,13 @@ def _build(self, attn_implementation="sdpa"): return ESMFold2Model(config).eval() def test_forward_runs_on_both_backends(self): - # No ESMC backbone is loaded -> LM conditioning is skipped (a valid path), - # so this exercises the full pure-PyTorch structural stack on CPU. + # The ESMC backbone is a bundled (tiny, randomly-initialised) submodule, so this + # exercises the full pure-PyTorch stack on CPU end-to-end: backbone + trunk + + # diffusion + confidence head. for impl in ("sdpa", "eager"): with self.subTest(attn_implementation=impl): model = self._build(impl) - self.assertIsNone(model._esmc) + self.assertIsInstance(model.esmc, torch.nn.Module) with torch.no_grad(): out = model.infer_protein(self.seq, num_loops=1, num_diffusion_samples=1, num_sampling_steps=2) coords = out["sample_atom_coords"] @@ -166,7 +166,7 @@ def test_forward_runs_on_both_backends(self): def test_attention_dispatch_attached(self): model = self._build("eager") - swa_modules = [m for m in model.modules() if isinstance(m, SWA3DRoPEAttention)] + swa_modules = [m for m in model.modules() if isinstance(m, ESMFold2SWA3DRoPEAttention)] # Both atom sites (inputs embedder + diffusion decoder) contribute SWA modules. self.assertGreaterEqual(len(swa_modules), 1) self.assertTrue(all(m.config is model.config for m in swa_modules)) @@ -181,9 +181,9 @@ def test_save_load(self): with tempfile.TemporaryDirectory() as tmp: model.save_pretrained(tmp) - # load_esmc=False: skip auto-loading the (real, multi-GB) ESMC backbone; - # the saved model has no backbone either, so both take the no-LM path. - reloaded = ESMFold2Model.from_pretrained(tmp, load_esmc=False).eval() + # The (tiny) ESMC backbone is bundled in the saved checkpoint and reloaded + # like any other submodule — no separate backbone load. + reloaded = ESMFold2Model.from_pretrained(tmp).eval() state_after = reloaded.state_dict() self.assertEqual(set(state_before), set(state_after)) @@ -200,8 +200,8 @@ class ESMFold2IntegrationTest(TestCasePlus): @slow @require_torch_accelerator def test_inference_protein_folding(self): - # bf16 is the intended inference regime; from_pretrained auto-loads the - # ESMC backbone (load_esmc=True by default). + # bf16 is the intended inference regime; the ESMC backbone is bundled in the + # checkpoint and loaded with the model. model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).to(torch_device).eval() # Ubiquitin (PDB 1UBQ), a textbook well-folding 76-residue domain. These @@ -224,3 +224,92 @@ def test_inference_protein_folding(self): best_ptm = ptm.max().item() self.assertGreater(best_plddt, 0.7) self.assertGreater(best_ptm, 0.6) + + @slow + def test_inference_deterministic_cpu_fp32(self): + # The test above runs in the intended bf16/GPU regime, but GPU bf16 is not + # reproducible run-to-run (matmul nondeterminism amplified through the deep + # trunk swings the distogram logits by O(1)), so it can only assert loose + # confidence floors. CPU/fp32 is bit-exact run-to-run, so here we lock in + # the actual values to a tight tolerance to catch *small* drift from future + # changes. distogram_logits is the cleanest signal: it is computed from the + # trunk with no diffusion sampling, so it is RNG-independent and exercises + # the ESMC-6B backbone + LM projection + pairformer trunk end-to-end. + # + # Baked from biohub/ESMFold2 @ dd1eae4fb2 (torch 2.11+cu130). A standalone + # multi-sequence version of this check lives in esmfold2_regression.py. + model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.float32).eval() + + seq = "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG" + torch.manual_seed(0) + with torch.no_grad(): + output = model.infer_protein(seq, num_loops=4, num_diffusion_samples=2, num_sampling_steps=32) + + # TODO(pre-merge): EXACT (atol=0) values are bit-exact only on the machine + # they were baked on (RTX 4090, torch 2.11+cu130); fp32 bits can differ across + # CPUs/BLAS. Before merging, loosen to a rounded slice with rtol/atol ~1e-3 + # (e.g. [6.5849, 7.9825, 9.6068, 9.6403, 16.5200, 18.9912, 19.9698, 23.0489]). + expected_distogram = torch.tensor( + [ + 6.584877014160156, + 7.982535362243652, + 9.60677433013916, + 9.640305519104004, + 16.519989013671875, + 18.99123764038086, + 19.96978187561035, + 23.04886817932129, + ] + ) + torch.testing.assert_close(output["distogram_logits"][0, 0, 1, :8].float(), expected_distogram, rtol=0, atol=0) + self.assertEqual(output["ptm"].max().item(), 0.7425990104675293) + + @slow + @require_torch_accelerator + def test_inference_deterministic_bf16(self): + # bf16/GPU is the production regime AND the one most exposed to dtype bugs + # (an op run in bf16 where the fork uses fp32, or vice-versa). Such bugs only + # surface in bf16, so we lock this path too. bf16/GPU with default kernels is + # not reproducible run-to-run, but with deterministic algorithms it becomes + # bit-exact (run-to-run AND cross-process), so we can baked-compare it tightly: + # a representative dtype regression (an fp32-pinned norm computing in bf16) + # shifts these distogram values by ~0.4-2.0, far above the deterministic floor. + # + # Baked from biohub/ESMFold2 @ dd1eae4fb2 on an RTX 4090 (torch 2.11+cu130). + # The exact bf16 bits are hardware-specific; the tolerance is loose enough to + # catch dtype drift but a different GPU may need a re-bake. esmfold2_regression.py + # locks all four sequences this way on the local box (CUBLAS_WORKSPACE_CONFIG=:4096:8 + # may be needed on some builds for fully deterministic cuBLAS). + prev = ( + torch.are_deterministic_algorithms_enabled(), + torch.is_deterministic_algorithms_warn_only_enabled(), + torch.backends.cudnn.deterministic, + torch.backends.cudnn.benchmark, + torch.backends.cuda.matmul.allow_tf32, + ) + try: + torch.use_deterministic_algorithms(True, warn_only=True) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + torch.backends.cuda.matmul.allow_tf32 = False + + model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).to(torch_device).eval() + seq = "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG" + torch.manual_seed(0) + with torch.no_grad(): + output = model.infer_protein(seq, num_loops=4, num_diffusion_samples=2, num_sampling_steps=32) + + # TODO(pre-merge): EXACT (atol=0) values are bit-exact only on the machine + # they were baked on (RTX 4090, torch 2.11+cu130); bf16 bits differ across + # GPUs. Before merging, loosen to atol~0.2 (catches dtype-swap drift of + # ~0.4-2.0 while absorbing cross-GPU variation). + expected_distogram = torch.tensor([6.46875, 7.71875, 9.4375, 9.3125, 16.125, 18.625, 19.625, 22.625]) + torch.testing.assert_close( + output["distogram_logits"][0, 0, 1, :8].float().cpu(), expected_distogram, rtol=0, atol=0 + ) + self.assertEqual(output["ptm"].max().item(), 0.7421817183494568) + finally: + torch.use_deterministic_algorithms(prev[0], warn_only=prev[1]) + torch.backends.cudnn.deterministic = prev[2] + torch.backends.cudnn.benchmark = prev[3] + torch.backends.cuda.matmul.allow_tf32 = prev[4] diff --git a/utils/check_repo.py b/utils/check_repo.py index 00afb1ea234b..aee25480f895 100644 --- a/utils/check_repo.py +++ b/utils/check_repo.py @@ -654,8 +654,6 @@ def get_model_modules() -> list[str]: _ignore_modules = [ "modeling_auto", "modeling_encoder_decoder", - # Shared ESMFold2 building blocks; the public model is in modeling_esmfold2. - "modeling_esmfold2_common", "modeling_marian", "modeling_retribert", "modeling_speech_encoder_decoder", From bbf8d8d1e52ff5bc5c2c584c64da2a78aa44bd4f Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 16:47:03 +0100 Subject: [PATCH 38/70] Remove a lot of redundant dtype casts now that we no longer need them --- .../models/esmfold2/modeling_esmfold2.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 14931974c735..ba0aa5898cec 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -20,8 +20,9 @@ model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() open("ubq.pdb", "w").write(model.infer_protein_as_pdb("MQIFVKTLTGKT...")) -For multi-chain / ligand / MSA inputs see ``ESMFold2InputBuilder`` in the -companion ``esm`` package. +``infer_protein`` / ``infer_protein_as_pdb`` cover single-chain protein folding from a +sequence; multi-chain, ligand, or MSA inputs are supported by building the structural +feature tensors yourself and passing them directly to ``forward``. """ from __future__ import annotations @@ -230,7 +231,7 @@ def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: self.out_proj = nn.Linear(hidden, d_model, bias=False) def forward(self, x: Tensor) -> Tensor: - x = self.norm(x.float()).to(x.dtype) + x = self.norm(x) a = self.a_proj(x) b = self.b_proj(x) return self.out_proj(F.silu(a) * b) @@ -913,7 +914,7 @@ def forward( q_l = result intermediates = [] - r_l = self.output_linear(self.norm(q_l.float()).to(q_l.dtype)) + r_l = self.output_linear(self.norm(q_l)) return r_l, intermediates @@ -967,7 +968,7 @@ def compute_pair_bias(self, z: Tensor, bsz: int, num_diffusion_samples: int = 1) if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: z = z.repeat_interleave(num_diffusion_samples, dim=0) if z.dim() == 4: - return self.pair_bias_proj(self.pair_norm(z.float()).to(z.dtype)) + return self.pair_bias_proj(self.pair_norm(z)) return z.unsqueeze(-1) def forward( @@ -984,7 +985,7 @@ def forward( if s is not None: x = self.adaln(a, s) else: - x = self.pre_norm(a.float()).to(a.dtype) + x = self.pre_norm(a) n_keys = x.shape[1] q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) @@ -1055,7 +1056,7 @@ def forward(self, a: Tensor, s: Tensor | None) -> Tensor: if s is not None: x = self.adaln(a, s) else: - x = self.pre_norm(a.float()).to(a.dtype) + x = self.pre_norm(a) swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) b = F.silu(swish_a) * swish_b @@ -1389,7 +1390,7 @@ def forward( ) # Step 4: add conditioned s - a = a + self.s_to_token(self.s_step_norm(s.float()).to(s.dtype)) + a = a + self.s_to_token(self.s_step_norm(s)) # Step 5: token transformer (pair bias is cached across steps via inference_cache) a, _ = self.token_transformer( @@ -1402,7 +1403,7 @@ def forward( ) # Step 6: token norm - a = self.token_norm(a.float()).to(a.dtype) + a = self.token_norm(a) # Step 7: atom decoder r_update, dec_intermediates = self.atom_decoder( @@ -1943,13 +1944,13 @@ def forward(self, hidden_states: Tensor) -> Tensor: # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. - normed = self.base_z_linear[0](hidden_states.float()).to(hidden_states.dtype) + normed = self.base_z_linear[0](hidden_states) lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] weights = self.base_z_combine.softmax(0) # [81] lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. pair = self.base_z_mlp[0](lm_z) - lm_z = self.base_z_mlp[1](pair.float()).to(pair.dtype) # [B, L, L, d_z] + lm_z = self.base_z_mlp[1](pair) # [B, L, L, d_z] return lm_z @@ -2129,7 +2130,7 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor if visibility is None: visibility = pair_grid.new_ones(pair_grid.shape[:-1]) - normalized_grid = self.norm_start(pair_grid.float()).to(pair_grid.dtype) + normalized_grid = self.norm_start(pair_grid) bundled = self.proj_bundle(normalized_grid) signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) # Gates and the O(N^3) contraction run in the activation dtype (bf16). This @@ -2186,12 +2187,12 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def forward(self, x: Tensor) -> Tensor: if self._chunk_size is None or x.shape[1] <= self._chunk_size: - return x + self.ffn(self.norm(x.float()).to(x.dtype)) + return x + self.ffn(self.norm(x)) out_list: list[Tensor] = [] for s in range(0, x.shape[1], self._chunk_size): e = min(s + self._chunk_size, x.shape[1]) sl = x[:, s:e] - out_list.append(sl + self.ffn(self.norm(sl.float()).to(sl.dtype))) + out_list.append(sl + self.ffn(self.norm(sl))) return torch.cat(out_list, dim=1) @@ -2278,7 +2279,7 @@ def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: - m_norm = self.norm(m.float()).to(m.dtype) + m_norm = self.norm(m) x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) a, b = x.chunk(2, dim=-1) mask_f = msa_attention_mask.to(a.dtype) @@ -2329,8 +2330,8 @@ def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tens B, L, M, _ = msa_repr.shape h, dh = self.n_heads, self.head_width - msa_normed = self.norm_single(msa_repr.float()).to(msa_repr.dtype) - bias = self.compute_bias[1](self.compute_bias[0](pair_repr.float()).to(pair_repr.dtype)) # [B, L, L, n_heads] + msa_normed = self.norm_single(msa_repr) + bias = self.compute_bias[1](self.compute_bias[0](pair_repr)) # [B, L, L, n_heads] bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j @@ -2480,9 +2481,9 @@ def forward( relative_position_encoding: Tensor | None = None, token_bonds_encoding: Tensor | None = None, ) -> dict[str, Tensor]: - s_inputs_normed = self.s_inputs_norm(s_inputs.float()).to(s_inputs.dtype) + s_inputs_normed = self.s_inputs_norm(s_inputs) - z_base = self.z_norm(z.float()).to(z.dtype) + z_base = self.z_norm(z) if relative_position_encoding is not None: z_base = z_base + relative_position_encoding if token_bonds_encoding is not None: @@ -2519,7 +2520,7 @@ def forward( atom_mask_f = atom_mask_m.float() s_at_atoms = gather_token_to_atom(single, atom_to_token_m) - s_at_atoms_ln = self.plddt_ln(s_at_atoms.float()).to(s_at_atoms.dtype) + s_at_atoms_ln = self.plddt_ln(s_at_atoms) intra_idx = _compute_intra_token_idx(atom_to_token_m) intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) @@ -2555,15 +2556,15 @@ def forward( plddt_ca = plddt_per_atom.gather(1, rep_idx_m) # PAE - pae_logits = self.pae_head(self.pae_ln(pair.float()).to(pair.dtype)) + pae_logits = self.pae_head(self.pae_ln(pair)) pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() # PDE - pde_logits = self.pde_head(self.pde_ln(pair.float()).to(pair.dtype)) + pde_logits = self.pde_head(self.pde_ln(pair)) pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() # Resolved (per-atom binary). - s_at_atoms_res = self.resolved_ln(s_at_atoms.float()).to(s_at_atoms.dtype) + s_at_atoms_res = self.resolved_ln(s_at_atoms) w_res = self.resolved_weight[intra_idx] resolved_logits = torch.einsum("...c,...cb->...b", s_at_atoms_res, w_res) @@ -2889,7 +2890,7 @@ def _run_one_loop( if refined_lm_z is not None: z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) - injected_pair = self.parcae_input_norm(z_inject_pair.float()).to(z_inject_pair.dtype) + injected_pair = self.parcae_input_norm(z_inject_pair) z = a * z + F.linear(injected_pair.to(z.dtype), b_mat) z = self.folding_trunk(z, pair_attention_mask=pair_mask) From e7d8a1782f31e3176a81f973f8808dd2c67be9f4 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 17:22:10 +0100 Subject: [PATCH 39/70] Rename a lot of config attributes to the standard ones --- docs/source/en/model_doc/esmc.md | 2 +- docs/source/en/model_doc/esmfold2.md | 2 +- .../models/esmc/configuration_esmc.py | 17 ++++++---- .../models/esmfold2/configuration_esmfold2.py | 33 ++++++++++++++----- .../models/esmfold2/modeling_esmfold2.py | 8 ++--- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 792ba47df906..05ea1a7a7558 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-04.* +*This model was contributed to Hugging Face Transformers on 2026-06-12.* # ESMC diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 83584e874841..469996572264 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-04.* +*This model was contributed to Hugging Face Transformers on 2026-06-12.* # ESMFold2 diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py index a172de73b1ed..891fdf5f439d 100644 --- a/src/transformers/models/esmc/configuration_esmc.py +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -23,11 +23,11 @@ @strict class ESMCConfig(PreTrainedConfig): r""" - d_model (`int`, *optional*, defaults to 2560): + hidden_size (`int`, *optional*, defaults to 2560): Dimensionality of the encoder layers and the pooler layer. - n_heads (`int`, *optional*, defaults to 40): + num_attention_heads (`int`, *optional*, defaults to 40): Number of attention heads for each attention layer in the Transformer encoder. - n_layers (`int`, *optional*, defaults to 80): + num_hidden_layers (`int`, *optional*, defaults to 80): Number of hidden layers in the Transformer encoder. mask_token_id (`int`, *optional*, defaults to 32): Index of the mask token in the vocabulary (``""``), used for masked language modelling. @@ -53,11 +53,16 @@ class ESMCConfig(PreTrainedConfig): """ model_type = "esmc" + attribute_map = { + "d_model": "hidden_size", + "n_heads": "num_attention_heads", + "n_layers": "num_hidden_layers", + } vocab_size: int | None = 64 - d_model: int | None = 2560 - n_heads: int | None = 40 - n_layers: int | None = 80 + hidden_size: int | None = 2560 + num_attention_heads: int | None = 40 + num_hidden_layers: int | None = 80 pad_token_id: int | None = 1 mask_token_id: int | None = 32 initializer_range: float | None = 0.02 diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index acdacd6c7309..760360314314 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -32,11 +32,16 @@ class MSAEncoderConfig(PreTrainedConfig): """Config for the optional MSA encoder module (Large MSA models only).""" + attribute_map = { + "n_layers": "num_hidden_layers", + "n_heads_msa": "num_attention_heads", + } + enabled: bool | None = False d_msa: int | None = 128 d_hidden: int | None = 32 - n_layers: int | None = 4 - n_heads_msa: int | None = 8 + num_hidden_layers: int | None = 4 + num_attention_heads: int | None = 8 msa_head_width: int | None = 32 @@ -52,8 +57,10 @@ class ParcaeConfig(PreTrainedConfig): class LMEncoderConfig(PreTrainedConfig): """Release-only config for the LM-side pair encoder.""" + attribute_map = {"n_layers": "num_hidden_layers"} + enabled: bool | None = True - n_layers: int | None = 4 + num_hidden_layers: int | None = 4 lm_dropout: float | None = 0.25 per_loop_lm_dropout: bool | None = True @@ -62,10 +69,15 @@ class LMEncoderConfig(PreTrainedConfig): class AtomAttentionConfig(PreTrainedConfig): """Config for the SWA atom encoder/decoder with 3D RoPE.""" + attribute_map = { + "n_blocks": "num_hidden_layers", + "n_heads": "num_attention_heads", + } + d_atom: int | None = 128 d_token: int | None = 768 - n_blocks: int | None = 3 - n_heads: int | None = 4 + num_hidden_layers: int | None = 3 + num_attention_heads: int | None = 4 swa_window_size: int | None = 128 expansion_ratio: int | None = 2 spatial_rope_base_frequency: float | None = 20.0 @@ -78,8 +90,13 @@ class AtomAttentionConfig(PreTrainedConfig): class FoldingTrunkConfig(PreTrainedConfig): """Config for a pairwise folding trunk stack.""" - n_layers: int | None = 24 - n_heads: int | None = 8 + attribute_map = { + "n_layers": "num_hidden_layers", + "n_heads": "num_attention_heads", + } + + num_hidden_layers: int | None = 24 + num_attention_heads: int | None = 8 dropout: float | None = 0.0 @@ -161,7 +178,7 @@ class ConfidenceHeadConfig(PreTrainedConfig): def __post_init__(self, **kwargs): if self.folding_trunk is None: - self.folding_trunk = FoldingTrunkConfig(n_layers=4) + self.folding_trunk = FoldingTrunkConfig(num_hidden_layers=4) elif isinstance(self.folding_trunk, dict): self.folding_trunk = FoldingTrunkConfig(**self.folding_trunk) super().__post_init__(**kwargs) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index ba0aa5898cec..f1c14599753d 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -1926,9 +1926,7 @@ def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> super().__init__() self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) - self.base_z_linear = nn.Sequential( - ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False) - ) + self.base_z_linear = nn.Sequential(ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) def forward(self, hidden_states: Tensor) -> Tensor: @@ -2311,9 +2309,7 @@ def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = self.n_heads = n_heads self.head_width = head_width self.norm_single = ESMFold2LayerNorm(d_msa) - self.compute_bias = nn.Sequential( - ESMFold2LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False) - ) + self.compute_bias = nn.Sequential(ESMFold2LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) From 5c436c4a5cf5d1a79c30a80351dcec0efc550f18 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 18:07:27 +0100 Subject: [PATCH 40/70] Modular cleanup, use standard output types --- src/transformers/models/esmc/modeling_esmc.py | 108 ++++++++---------- src/transformers/models/esmc/modular_esmc.py | 94 +++++---------- .../models/esmfold2/modeling_esmfold2.py | 4 +- tests/models/esmc/test_modeling_esmc.py | 7 -- 4 files changed, 81 insertions(+), 132 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index c6c81582e117..f533ae94c2be 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -28,10 +28,11 @@ from torch.nn import functional as F from ... import initialization as init +from ...integrations import use_kernel_func_from_hub from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] + BaseModelOutput, MaskedLMOutput, - ModelOutput, SequenceClassifierOutput, TokenClassifierOutput, ) @@ -46,28 +47,6 @@ # --------------------------------------------------------------------------- -@dataclass -class ESMCOutput(ModelOutput): - """ - Args: - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Sequence of hidden states at the output of the last layer, after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states for all encoder layers. - Shape ``(n_layers, batch_size, sequence_length, d_model)``. - Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. Not available on the - ``flash_attention_2`` path. - """ - - last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - @dataclass class ESMCMaskedLMOutput(MaskedLMOutput): """ @@ -78,8 +57,9 @@ class ESMCMaskedLMOutput(MaskedLMOutput): Prediction scores of the language modelling head. last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): Final hidden states after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + hidden_states (`tuple(torch.FloatTensor)`, *optional*): + Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` + (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -89,7 +69,7 @@ class ESMCMaskedLMOutput(MaskedLMOutput): loss: torch.FloatTensor | None = None logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None + hidden_states: tuple[torch.FloatTensor, ...] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -103,8 +83,9 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): Classification scores (before SoftMax). last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): Final hidden states after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + hidden_states (`tuple(torch.FloatTensor)`, *optional*): + Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` + (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -114,7 +95,7 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): loss: torch.FloatTensor | None = None logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None + hidden_states: tuple[torch.FloatTensor, ...] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -128,8 +109,9 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): Classification scores (before SoftMax). last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): Final hidden states after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + hidden_states (`tuple(torch.FloatTensor)`, *optional*): + Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` + (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -139,7 +121,7 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): loss: torch.FloatTensor | None = None logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None + hidden_states: tuple[torch.FloatTensor, ...] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -287,6 +269,32 @@ def rotate_half(x): return torch.cat((-x2, x1), dim=-1) +@use_kernel_func_from_hub("rotary_pos_emb") +def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): + """Applies Rotary Position Embedding to the query and key tensors. + + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_embed = (q * cos) + (rotate_half(q) * sin) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + def eager_attention_forward( module: nn.Module, query: torch.Tensor, @@ -315,26 +323,6 @@ def eager_attention_forward( return attn_output, attn_weights -def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): - """Apply Rotary Position Embedding to ``q`` and ``k`` in the activation dtype. - - This deliberately differs from the otherwise-identical - :func:`~transformers.models.esm.modeling_esm.apply_rotary_pos_emb` (whose - ``rotate_half`` it reuses): that helper upcasts ``q``/``k`` to fp32 for the - rotation, but the reference ESMC implementation applies RoPE in the - activation dtype. Upcasting here would make bf16 inference diverge from the - published ESMC numerics — at bf16 it is the single source of fork-vs-port - drift, accumulating over the residual stream (~0.3 over 80 layers on - ESMC-6B). The rotation is a no-op-difference in fp32 (``q`` is already fp32), - so fp32 stays bit-exact. See ``modeling_esm`` for the argument semantics. - """ - cos = cos.unsqueeze(unsqueeze_dim) - sin = sin.unsqueeze(unsqueeze_dim) - q_embed = (q * cos) + (rotate_half(q) * sin) - k_embed = (k * cos) + (rotate_half(k) * sin) - return q_embed, k_embed - - # --------------------------------------------------------------------------- # Attention # --------------------------------------------------------------------------- @@ -708,7 +696,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, - ) -> tuple[torch.Tensor, ...] | ESMCOutput: + ) -> tuple[torch.Tensor, ...] | BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor for chain-aware attention masking. Tokens with the same @@ -785,25 +773,25 @@ def forward( output_attentions=output_attentions, ) - collected_tensor: torch.Tensor | None = ( - torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] - ) - hidden_states_tensor = collected_tensor if output_hidden_states else None + # Standard Transformers convention: a tuple of per-layer hidden states (the + # live activations collected in the encoder), not a stacked tensor. Consumers + # that want a single tensor (e.g. ESMFold2's LM projection) stack it themselves. + hidden_states = collected if output_hidden_states else None if not return_dict: return tuple( v for v in [ last_hidden_state, - hidden_states_tensor, + hidden_states, attentions, ] if v is not None ) - return ESMCOutput( + return BaseModelOutput( last_hidden_state=last_hidden_state, - hidden_states=hidden_states_tensor, + hidden_states=hidden_states, attentions=attentions, ) diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 26a391aef17b..12195a05aeca 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -25,8 +25,8 @@ from ... import initialization as init from ...masking_utils import create_bidirectional_mask # type: ignore[import] from ...modeling_outputs import ( # type: ignore[import] + BaseModelOutput, MaskedLMOutput, - ModelOutput, SequenceClassifierOutput, TokenClassifierOutput, ) @@ -36,10 +36,15 @@ can_return_tuple, logging, ) -from ..esm.modeling_esm import ( - eager_attention_forward, - rotate_half, -) +from ..esm.modeling_esm import eager_attention_forward + +# ESMC applies RoPE in the activation dtype (no fp32 upcast), matching the reference +# implementation's bf16 numerics. Llama's `apply_rotary_pos_emb` is exactly that +# no-upcast variant; `esm`'s upcasts q/k to fp32 and would drift bf16 inference (the +# dominant fork-vs-port divergence on the ESMFold2 backbone, ~0.3 over 80 layers on +# ESMC-6B), so we reuse Llama's rather than esm's. `rotate_half` is pulled in by the +# modular converter as a dependency of the imported function. +from ..llama.modeling_llama import apply_rotary_pos_emb from .configuration_esmc import ESMCConfig @@ -52,28 +57,6 @@ # --------------------------------------------------------------------------- -@dataclass -class ESMCOutput(ModelOutput): - """ - Args: - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Sequence of hidden states at the output of the last layer, after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states for all encoder layers. - Shape ``(n_layers, batch_size, sequence_length, d_model)``. - Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. Not available on the - ``flash_attention_2`` path. - """ - - last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - @dataclass class ESMCMaskedLMOutput(MaskedLMOutput): """ @@ -84,8 +67,9 @@ class ESMCMaskedLMOutput(MaskedLMOutput): Prediction scores of the language modelling head. last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): Final hidden states after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + hidden_states (`tuple(torch.FloatTensor)`, *optional*): + Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` + (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -95,7 +79,7 @@ class ESMCMaskedLMOutput(MaskedLMOutput): loss: torch.FloatTensor | None = None logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None + hidden_states: tuple[torch.FloatTensor, ...] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -109,8 +93,9 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): Classification scores (before SoftMax). last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): Final hidden states after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + hidden_states (`tuple(torch.FloatTensor)`, *optional*): + Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` + (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -120,7 +105,7 @@ class ESMCTokenClassifierOutput(TokenClassifierOutput): loss: torch.FloatTensor | None = None logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None + hidden_states: tuple[torch.FloatTensor, ...] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -134,8 +119,9 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): Classification scores (before SoftMax). last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): Final hidden states after layer normalisation. - hidden_states (`torch.FloatTensor`, *optional*): - Stacked hidden states. Shape ``(n_layers, batch_size, sequence_length, d_model)``. + hidden_states (`tuple(torch.FloatTensor)`, *optional*): + Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` + (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. attentions (`tuple(torch.FloatTensor)`, *optional*): Per-layer attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -145,7 +131,7 @@ class ESMCSequenceClassifierOutput(SequenceClassifierOutput): loss: torch.FloatTensor | None = None logits: torch.FloatTensor | None = None last_hidden_state: torch.FloatTensor | None = None - hidden_states: torch.FloatTensor | None = None + hidden_states: tuple[torch.FloatTensor, ...] | None = None attentions: tuple[torch.FloatTensor, ...] | None = None @@ -202,26 +188,6 @@ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Te return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) -def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): - """Apply Rotary Position Embedding to ``q`` and ``k`` in the activation dtype. - - This deliberately differs from the otherwise-identical - :func:`~transformers.models.esm.modeling_esm.apply_rotary_pos_emb` (whose - ``rotate_half`` it reuses): that helper upcasts ``q``/``k`` to fp32 for the - rotation, but the reference ESMC implementation applies RoPE in the - activation dtype. Upcasting here would make bf16 inference diverge from the - published ESMC numerics — at bf16 it is the single source of fork-vs-port - drift, accumulating over the residual stream (~0.3 over 80 layers on - ESMC-6B). The rotation is a no-op-difference in fp32 (``q`` is already fp32), - so fp32 stays bit-exact. See ``modeling_esm`` for the argument semantics. - """ - cos = cos.unsqueeze(unsqueeze_dim) - sin = sin.unsqueeze(unsqueeze_dim) - q_embed = (q * cos) + (rotate_half(q) * sin) - k_embed = (k * cos) + (rotate_half(k) * sin) - return q_embed, k_embed - - # --------------------------------------------------------------------------- # Feed-forward network helpers # --------------------------------------------------------------------------- @@ -679,7 +645,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, - ) -> tuple[torch.Tensor, ...] | ESMCOutput: + ) -> tuple[torch.Tensor, ...] | BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor for chain-aware attention masking. Tokens with the same @@ -756,25 +722,25 @@ def forward( output_attentions=output_attentions, ) - collected_tensor: torch.Tensor | None = ( - torch.stack(collected, dim=0) if collected else None # type: ignore[arg-type] - ) - hidden_states_tensor = collected_tensor if output_hidden_states else None + # Standard Transformers convention: a tuple of per-layer hidden states (the + # live activations collected in the encoder), not a stacked tensor. Consumers + # that want a single tensor (e.g. ESMFold2's LM projection) stack it themselves. + hidden_states = collected if output_hidden_states else None if not return_dict: return tuple( v for v in [ last_hidden_state, - hidden_states_tensor, + hidden_states, attentions, ] if v is not None ) - return ESMCOutput( + return BaseModelOutput( last_hidden_state=last_hidden_state, - hidden_states=hidden_states_tensor, + hidden_states=hidden_states, attentions=attentions, ) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index f1c14599753d..5b8f9a938e49 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -2052,7 +2052,9 @@ def compute_lm_hidden_states( with torch.inference_mode(): esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) - hs = esmc_out.hidden_states # [n_layers+1, B, max_len, D] + # ESMC returns hidden states as the standard tuple of per-layer tensors; stack + # them into the single [n_layers+1, B, max_len, D] tensor the projection expects. + hs = torch.stack(esmc_out.hidden_states, dim=0) # [n_layers+1, B, max_len, D] n_layers_plus_1, _, _, D = hs.shape result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) for b in range(B): diff --git a/tests/models/esmc/test_modeling_esmc.py b/tests/models/esmc/test_modeling_esmc.py index 83b60890d5a6..b1527091e2bd 100644 --- a/tests/models/esmc/test_modeling_esmc.py +++ b/tests/models/esmc/test_modeling_esmc.py @@ -185,13 +185,6 @@ def test_for_token_classification(self): config_and_inputs = self.model_tester.prepare_config_and_inputs() self.model_tester.create_and_check_for_token_classification(*config_and_inputs) - @unittest.skip( - reason="ESMC returns `hidden_states` as a single stacked tensor (used by the SAE feature), " - "not the live per-layer tensors, so grad does not flow back to the returned copy." - ) - def test_retain_grad_hidden_states_attentions(self): - pass - @slow @require_torch From 61d175fe79bf969fc30a0258d499d7b69e62c5df Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 18:49:12 +0100 Subject: [PATCH 41/70] More modular cleanup --- src/transformers/models/esmc/modeling_esmc.py | 36 - src/transformers/models/esmc/modular_esmc.py | 36 - .../models/esmfold2/modeling_esmfold2.py | 763 +++++++++--------- 3 files changed, 369 insertions(+), 466 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index f533ae94c2be..1ce365fe2343 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -615,11 +615,6 @@ def forward( @auto_docstring class ESMCPreTrainedModel(PreTrainedModel): - """Base class for ESMC models. - - Handles weight initialisation and declares module-level capabilities. - """ - config_class = ESMCConfig base_model_prefix = "esmc" supports_gradient_checkpointing = False @@ -657,17 +652,6 @@ def _init_weights(self, module: nn.Module): @auto_docstring class ESMCModel(ESMCPreTrainedModel): - """The bare ESMC encoder outputting raw hidden states. - - ESMC is a protein language model trained by EvolutionaryScale using a - masked-token objective over amino acid sequences. The architecture is a - standard Transformer encoder with RoPE positional embeddings, QK LayerNorm, - and SwiGLU feed-forward networks. - - Args: - config: An :class:`ESMCConfig` instance. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) @@ -830,13 +814,6 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: @auto_docstring class ESMCForMaskedLM(ESMCPreTrainedModel): - """ESMC with a masked language modelling head. - - This is the primary pre-training objective of ESMC. The LM head consists - of a single hidden layer with GELU activation followed by LayerNorm and a - linear projection to ``vocab_size``. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.esmc = ESMCModel(config) @@ -954,13 +931,6 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: @auto_docstring class ESMCForSequenceClassification(ESMCPreTrainedModel): - """ESMC with a sequence-level classification head. - - A linear layer is applied to the ```` token representation. - Supports regression (``num_labels == 1``), single-label classification, - and multi-label classification. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.num_labels = config.num_labels @@ -1051,12 +1021,6 @@ def forward( @auto_docstring class ESMCForTokenClassification(ESMCPreTrainedModel): - """ESMC with a per-token classification head. - - Useful for tasks such as secondary structure prediction, contact-map - prediction, or per-residue labelling. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.num_labels = config.num_labels diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 12195a05aeca..2f6ca0f99a55 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -564,11 +564,6 @@ def forward( @auto_docstring class ESMCPreTrainedModel(PreTrainedModel): - """Base class for ESMC models. - - Handles weight initialisation and declares module-level capabilities. - """ - config_class = ESMCConfig base_model_prefix = "esmc" supports_gradient_checkpointing = False @@ -606,17 +601,6 @@ def _init_weights(self, module: nn.Module): @auto_docstring class ESMCModel(ESMCPreTrainedModel): - """The bare ESMC encoder outputting raw hidden states. - - ESMC is a protein language model trained by EvolutionaryScale using a - masked-token objective over amino acid sequences. The architecture is a - standard Transformer encoder with RoPE positional embeddings, QK LayerNorm, - and SwiGLU feed-forward networks. - - Args: - config: An :class:`ESMCConfig` instance. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) @@ -779,13 +763,6 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: @auto_docstring class ESMCForMaskedLM(ESMCPreTrainedModel): - """ESMC with a masked language modelling head. - - This is the primary pre-training objective of ESMC. The LM head consists - of a single hidden layer with GELU activation followed by LayerNorm and a - linear projection to ``vocab_size``. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.esmc = ESMCModel(config) @@ -903,13 +880,6 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: @auto_docstring class ESMCForSequenceClassification(ESMCPreTrainedModel): - """ESMC with a sequence-level classification head. - - A linear layer is applied to the ```` token representation. - Supports regression (``num_labels == 1``), single-label classification, - and multi-label classification. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.num_labels = config.num_labels @@ -1000,12 +970,6 @@ def forward( @auto_docstring class ESMCForTokenClassification(ESMCPreTrainedModel): - """ESMC with a per-token classification head. - - Useful for tasks such as secondary structure prediction, contact-map - prediction, or per-residue labelling. - """ - def __init__(self, config: ESMCConfig): super().__init__(config) self.num_labels = config.num_labels diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 5b8f9a938e49..c567b373ee8a 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -1,3 +1,9 @@ +# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 +# This file was automatically generated from src/transformers/models/esmfold2/modular_esmfold2.py. +# Do NOT edit this file manually as any edits will be overwritten by the generation of +# the file from the modular. If any change should be done, please apply the change to the +# modular_esmfold2.py file directly. One of our CI enforces this. +# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 # Copyright 2026 Biohub. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,21 +17,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""PyTorch ESMFold2 model — the standard released architecture. - -Quickstart:: - - from transformers import ESMFold2Model - - model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() - open("ubq.pdb", "w").write(model.infer_protein_as_pdb("MQIFVKTLTGKT...")) - -``infer_protein`` / ``infer_protein_as_pdb`` cover single-chain protein folding from a -sequence; multi-chain, ligand, or MSA inputs are supported by building the structural -feature tensors yourself and passing them directly to ``forward``. -""" - -from __future__ import annotations import math from collections.abc import Callable @@ -38,158 +29,19 @@ from torch import Tensor from torch.utils.checkpoint import checkpoint - -try: - from flash_attn import ( - flash_attn_func, - flash_attn_varlen_func, - ) - from flash_attn.bert_padding import ( - index_first_axis, - pad_input, - ) - - FLASH_ATTN_AVAILABLE = True -except ImportError: - flash_attn_func = None - flash_attn_varlen_func = None - index_first_axis = None - pad_input = None - FLASH_ATTN_AVAILABLE = False - from ... import initialization as init from ...integrations import use_kernel_forward_from_hub from ...modeling_outputs import ModelOutput from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack -from ...utils import TransformersKwargs +from ...utils import TransformersKwargs, is_flash_attn_2_available from ..auto import AutoModel from .configuration_esmfold2 import ESMFold2Config -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- -CHAR_VOCAB_SIZE: int = 64 -MAX_CHARS: int = 4 -XYZ_DIMS: int = 3 -MAX_ATOMIC_NUMBER: int = 128 - -# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 -ATOM_FEATURE_DIM: int = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS - - -NUM_RES_TYPES: int = 33 - -_EPS = 1e-5 - -# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory -# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; -# chunk=64 leaves headroom for the largest foldbench targets). Override via -# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for -# short L but OOM-prone past ~600). -_DEFAULT_CHUNK_SIZE = 64 - - -# =========================================================================== -# Atom-token utilities -# =========================================================================== - - -def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: - """Broadcast per-token features to per-atom features using gather. - - Args: - token_features: [B, L, d] - atom_to_token_idx: [B, A] int64 - - Returns: - [B, A, d] - """ - idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) - return torch.gather(token_features, 1, idx) - - -def scatter_atom_to_token( - atom_features: Tensor, - atom_to_token_idx: Tensor, - n_tokens: int, - atom_mask: Tensor | None = None, -) -> Tensor: - """Aggregate per-atom features to per-token features (mean). - - Args: - atom_features: [B, A, d] - atom_to_token_idx: [B, A] int64 - n_tokens: L - atom_mask: [B, A] bool - - Returns: - [B, L, d] - """ - B, A, d = atom_features.shape - n_out = n_tokens - idx = atom_to_token_idx - if atom_mask is not None: - idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) - n_out = n_tokens + 1 - idx_expanded = idx.unsqueeze(-1).expand(B, A, d) - out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) - out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) - return out[:, :n_tokens, :] - - -def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: - """Gather representative atom coordinates for each token. - - Args: - coords: [B, A, 3] - rep_atom_idx: [B, L] int64 - - Returns: - [B, L, 3] - """ - idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) - return torch.gather(coords, 1, idx) - - -def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: - """Compute local atom index within each token (vectorised). - - Atoms belonging to the same token are contiguous, so this computes a - running count that resets at each token boundary. - - Args: - atom_to_token: [B, A] flat index mapping each atom to its token. - - Returns: - [B, A] tensor with values in [0, max_atoms_per_token - 1]. - """ - same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) - ones = torch.ones_like(atom_to_token) - cumsum = torch.cumsum(ones, dim=-1) - group_start = cumsum.masked_fill(same_as_prev, 0) - group_start = torch.cummax(group_start, dim=-1).values - return cumsum - group_start - - -def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: - """Expected value of a categorical distribution over evenly-spaced bins. - - Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. - - Args: - logits: [..., n_bins] - start: left boundary - end: right boundary - - Returns: - [...] expected value - """ - n_bins = logits.shape[-1] - edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) - v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] - return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) +if is_flash_attn_2_available(): + from flash_attn import flash_attn_func, flash_attn_varlen_func + from flash_attn.bert_padding import index_first_axis, pad_input # =========================================================================== @@ -324,83 +176,6 @@ def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) - super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) -# =========================================================================== -# SWA Atom Attention components -# =========================================================================== - - -# Copied from transformers.models.esm.modeling_esm.rotate_half -def rotate_half(x): - """Rotates half the hidden dims of the input.""" - x1 = x[..., : x.shape[-1] // 2] - x2 = x[..., x.shape[-1] // 2 :] - return torch.cat((-x2, x1), dim=-1) - - -def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: - """Apply RoPE with batch-dependent cos/sin. - - Args: - x: [B, L, H, D] - cos: [B, L, D/2] - sin: [B, L, D/2] - """ - ro_dim = cos.shape[-1] * 2 - cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) - sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) - return torch.cat( - [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], - dim=-1, - ) - - -@torch.compiler.disable -def build_3d_rope( - ref_pos: Tensor, - ref_space_uid: Tensor, - head_dim: int, - n_spatial_per_axis: int = 4, - n_uid_pairs: int = 2, - spatial_base_freq: float = 10000.0, - uid_base_freq: float = 10.0, -) -> tuple[Tensor, Tensor]: - """Build cos/sin for 3D RoPE + UID RoPE.""" - device = ref_pos.device - B, N = ref_pos.shape[:2] - half_dim = head_dim // 2 - n_spatial_total = 3 * n_spatial_per_axis - - spatial_inv_freq = 1.0 / ( - spatial_base_freq - ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) - ) - uid_inv_freq = 1.0 / ( - uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) - ) - - pos_f32 = ref_pos.float() - spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) - spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) - - uid_f32 = ref_space_uid.float() - uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) - - n_active = n_spatial_total + n_uid_pairs - freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) - - if n_active < half_dim: - padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) - freqs = torch.cat([freqs, padding], dim=-1) - - cos = freqs.cos().to(torch.bfloat16) - sin = freqs.sin().to(torch.bfloat16) - return cos, sin - - -def qk_norm(x: Tensor) -> Tensor: - return F.rms_norm(x, (x.size(-1),)) - - # =========================================================================== # ESMFold2SwiGLUFFN (atom transformer blocks) # =========================================================================== @@ -421,12 +196,13 @@ def forward(self, x: Tensor) -> Tensor: return self.w_down(F.silu(x1) * x2) -# =========================================================================== -# ESMFold2SWA3DRoPEAttention -# =========================================================================== +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) -# Copied from transformers.models.llama.modeling_llama.repeat_kv def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: """ This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, @@ -439,7 +215,6 @@ def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) -# Copied from transformers.models.llama.modeling_llama.eager_attention_forward def eager_attention_forward( module: nn.Module, query: torch.Tensor, @@ -465,6 +240,37 @@ def eager_attention_forward( return attn_output, attn_weights +# =========================================================================== +# SWA Atom Attention components +# =========================================================================== + + +def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: + """Apply RoPE with batch-dependent cos/sin. + + Args: + x: [B, L, H, D] + cos: [B, L, D/2] + sin: [B, L, D/2] + """ + ro_dim = cos.shape[-1] * 2 + cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) + sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) + return torch.cat( + [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], + dim=-1, + ) + + +def qk_norm(x: Tensor) -> Tensor: + return F.rms_norm(x, (x.size(-1),)) + + +# =========================================================================== +# ESMFold2SWA3DRoPEAttention +# =========================================================================== + + class ESMFold2SWA3DRoPEAttention(nn.Module): """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. @@ -513,7 +319,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" - use_flash = attn_impl == "flash_attention_2" and FLASH_ATTN_AVAILABLE + use_flash = attn_impl == "flash_attention_2" and is_flash_attn_2_available() if use_flash and len(attention_params) > 2: indices, cu_seqlens, max_seqlen = ( @@ -627,6 +433,49 @@ def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: return x +@torch.compiler.disable +def build_3d_rope( + ref_pos: Tensor, + ref_space_uid: Tensor, + head_dim: int, + n_spatial_per_axis: int = 4, + n_uid_pairs: int = 2, + spatial_base_freq: float = 10000.0, + uid_base_freq: float = 10.0, +) -> tuple[Tensor, Tensor]: + """Build cos/sin for 3D RoPE + UID RoPE.""" + device = ref_pos.device + B, N = ref_pos.shape[:2] + half_dim = head_dim // 2 + n_spatial_total = 3 * n_spatial_per_axis + + spatial_inv_freq = 1.0 / ( + spatial_base_freq + ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) + ) + uid_inv_freq = 1.0 / ( + uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) + ) + + pos_f32 = ref_pos.float() + spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) + spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) + + uid_f32 = ref_space_uid.float() + uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) + + n_active = n_spatial_total + n_uid_pairs + freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) + + if n_active < half_dim: + padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) + freqs = torch.cat([freqs, padding], dim=-1) + + cos = freqs.cos().to(torch.bfloat16) + sin = freqs.sin().to(torch.bfloat16) + return cos, sin + + class ESMFold2SWAAtomTransformer(nn.Module): """Stack of SWAAtomBlocks.""" @@ -690,6 +539,47 @@ def forward( return q_l +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +CHAR_VOCAB_SIZE = 64 +MAX_CHARS = 4 +XYZ_DIMS = 3 +MAX_ATOMIC_NUMBER = 128 + +# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 +ATOM_FEATURE_DIM = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS + + +def scatter_atom_to_token( + atom_features: Tensor, + atom_to_token_idx: Tensor, + n_tokens: int, + atom_mask: Tensor | None = None, +) -> Tensor: + """Aggregate per-atom features to per-token features (mean). + + Args: + atom_features: [B, A, d] + atom_to_token_idx: [B, A] int64 + n_tokens: L + atom_mask: [B, A] bool + + Returns: + [B, L, d] + """ + B, A, d = atom_features.shape + n_out = n_tokens + idx = atom_to_token_idx + if atom_mask is not None: + idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) + n_out = n_tokens + 1 + idx_expanded = idx.unsqueeze(-1).expand(B, A, d) + out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) + out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) + return out[:, :n_tokens, :] + + # =========================================================================== # ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) # =========================================================================== @@ -846,6 +736,25 @@ def forward( return a, q, c, attention_params, intermediates +# =========================================================================== +# Atom-token utilities +# =========================================================================== + + +def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: + """Broadcast per-token features to per-atom features using gather. + + Args: + token_features: [B, L, d] + atom_to_token_idx: [B, A] int64 + + Returns: + [B, A, d] + """ + idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) + return torch.gather(token_features, 1, idx) + + # =========================================================================== # ESMFold2AtomDecoder # =========================================================================== @@ -1910,161 +1819,56 @@ def forward(self, x: Tensor) -> Tensor: # =========================================================================== # ESMFold2LanguageModelShim -# =========================================================================== - - -class ESMFold2LanguageModelShim(nn.Module): - """Shim holding the trainable projection weights for LM integration. - - Contains: - - base_z_combine: nn.Parameter [num_layers+1] - - base_z_linear: Sequential(ESMFold2LayerNorm(d_model), Linear(d_model, d_z, bias=False)) - - base_z_mlp: Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) - """ - - def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: - super().__init__() - - self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) - self.base_z_linear = nn.Sequential(ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) - self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) - - def forward(self, hidden_states: Tensor) -> Tensor: - """Project pre-computed ESMC hidden states to pair representation. - - Args: - hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. - - Returns: - [B, L, L, d_pair] pair representation. - """ - # The ESMC backbone may be loaded at a different precision than the trunk - # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. - hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) - # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. - normed = self.base_z_linear[0](hidden_states) - lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] - weights = self.base_z_combine.softmax(0) # [81] - lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] - # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. - pair = self.base_z_mlp[0](lm_z) - lm_z = self.base_z_mlp[1](pair) # [B, L, L, d_z] - return lm_z - - -# =========================================================================== -# ESMFold2 — language-model backbone helpers -# =========================================================================== - - -def compute_lm_hidden_states( - esmc: nn.Module, - input_ids: Tensor, - asym_id: Tensor, - residue_index: Tensor, - mol_type: Tensor, - token_mask: Tensor, - pad_to_multiple: int | None = None, -) -> Tensor: - """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. - - Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple - structure tokens but share a single ``(asym_id, residue_index)`` key — - collapse them to one LM token per residue before running the LM (the LM - was trained on per-residue inputs, not per-atom), then scatter the - hidden states back to the per-token layout. - """ - B, L = input_ids.shape - device = input_ids.device - protein_mask = (mol_type == 0) & token_mask - - lm_input_list = [] - lm_lengths = [] - # Per-batch maps from (original protein-token index) to (LM input position). - expand_maps: list[Tensor] = [] - for b in range(B): - mask_b = protein_mask[b] - ids_b = input_ids[b][mask_b] - asym_b = asym_id[b][mask_b] - res_b = residue_index[b][mask_b] - - # Collapse: keep first token per (asym_id, residue_index) key, in - # input order. ``inverse`` maps each original protein-token to its - # collapsed residue index. - keys = torch.stack((asym_b, res_b), dim=1) - unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) - n_unique = unique_keys.size(0) - token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) - first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) - first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) - ordered = torch.argsort(first_pos) - first_pos_ordered = first_pos[ordered] - ids_collapsed = ids_b[first_pos_ordered] - asym_collapsed = asym_b[first_pos_ordered] - remap = torch.empty_like(ordered) - remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) - inverse_ordered = remap[inverse] - - chain_ids = asym_collapsed.unique(sorted=True) - # [BOS] chain1 [EOS BOS] chain2 ... [EOS] - parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] - # Per-chain LM positions accumulate; track them for the expand map. - per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) - cursor = 1 # position 0 is the leading BOS - for i, cid in enumerate(chain_ids): - in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] - parts.append(ids_collapsed[in_chain]) - per_token_lm_pos[in_chain] = torch.arange( - cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long - ) - cursor += in_chain.shape[0] - if i < len(chain_ids) - 1: - parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) - cursor += 2 # EOS + BOS - parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) - lm_seq = torch.cat(parts) - lm_input_list.append(lm_seq) - lm_lengths.append(lm_seq.shape[0]) +# =========================================================================== - # Original protein-token position → LM input position. - prot_pos_b = mask_b.nonzero(as_tuple=True)[0] - expand_map = torch.full((L,), -1, device=device, dtype=torch.long) - expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] - expand_maps.append(expand_map) - # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. - max_len = max(lm_lengths) - if pad_to_multiple is not None and pad_to_multiple > 1: - max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple - lm_input_ids = torch.full( - (B, max_len), - 1, - device=device, - dtype=input_ids.dtype, # PAD=1 - ) - for b in range(B): - lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] +class ESMFold2LanguageModelShim(nn.Module): + """Shim holding the trainable projection weights for LM integration. - # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). - sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 - sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 + Contains: + - base_z_combine: nn.Parameter [num_layers+1] + - base_z_linear: Sequential(ESMFold2LayerNorm(d_model), Linear(d_model, d_z, bias=False)) + - base_z_mlp: Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + """ - with torch.inference_mode(): - esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) + def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: + super().__init__() - # ESMC returns hidden states as the standard tuple of per-layer tensors; stack - # them into the single [n_layers+1, B, max_len, D] tensor the projection expects. - hs = torch.stack(esmc_out.hidden_states, dim=0) # [n_layers+1, B, max_len, D] - n_layers_plus_1, _, _, D = hs.shape - result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) - for b in range(B): - mb = protein_mask[b] - em = expand_maps[b][mb] # [n_protein_tokens] LM positions - # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] - gathered = hs[:, b, em, :].permute(1, 0, 2) - result[b, mb.nonzero(as_tuple=True)[0]] = gathered + self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + self.base_z_linear = nn.Sequential(ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) + self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) - return result.detach() + def forward(self, hidden_states: Tensor) -> Tensor: + """Project pre-computed ESMC hidden states to pair representation. + + Args: + hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. + + Returns: + [B, L, L, d_pair] pair representation. + """ + # The ESMC backbone may be loaded at a different precision than the trunk + # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. + hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) + # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. + normed = self.base_z_linear[0](hidden_states) + lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] + weights = self.base_z_combine.softmax(0) # [81] + lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] + # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. + pair = self.base_z_mlp[0](lm_z) + lm_z = self.base_z_mlp[1](pair) # [B, L, L, d_z] + return lm_z + + +_EPS = 1e-5 + +# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory +# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; +# chunk=64 leaves headroom for the largest foldbench targets). Override via +# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for +# short L but OOM-prone past ~600). +_DEFAULT_CHUNK_SIZE = 64 # =========================================================================== @@ -2340,10 +2144,6 @@ def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tens return self.Wout(output.reshape(B, L, M, h * dh)) -_CONFIDENCE_EPS = 1e-6 -_NONPOLYMER_ID = 4 - - @dataclass class ESMFold2Output(ModelOutput): """ @@ -2403,6 +2203,63 @@ class ESMFold2Output(ModelOutput): pair_chains_iptm: Tensor | None = None +def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: + """Gather representative atom coordinates for each token. + + Args: + coords: [B, A, 3] + rep_atom_idx: [B, L] int64 + + Returns: + [B, L, 3] + """ + idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) + return torch.gather(coords, 1, idx) + + +def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: + """Compute local atom index within each token (vectorised). + + Atoms belonging to the same token are contiguous, so this computes a + running count that resets at each token boundary. + + Args: + atom_to_token: [B, A] flat index mapping each atom to its token. + + Returns: + [B, A] tensor with values in [0, max_atoms_per_token - 1]. + """ + same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) + ones = torch.ones_like(atom_to_token) + cumsum = torch.cumsum(ones, dim=-1) + group_start = cumsum.masked_fill(same_as_prev, 0) + group_start = torch.cummax(group_start, dim=-1).values + return cumsum - group_start + + +def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: + """Expected value of a categorical distribution over evenly-spaced bins. + + Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. + + Args: + logits: [..., n_bins] + start: left boundary + end: right boundary + + Returns: + [...] expected value + """ + n_bins = logits.shape[-1] + edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) + v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] + return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) + + +_CONFIDENCE_EPS = 1e-6 +_NONPOLYMER_ID = 4 + + class ESMFold2ConfidenceHead(nn.Module): """Predicts pLDDT, PAE, PDE, resolved-atom probability and distogram bins.""" @@ -2675,6 +2532,124 @@ def _init_weights(self, module): init.zeros_(module.base_z_combine) +NUM_RES_TYPES = 33 + + +# =========================================================================== +# ESMFold2 — language-model backbone helpers +# =========================================================================== + + +def compute_lm_hidden_states( + esmc: nn.Module, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + token_mask: Tensor, + pad_to_multiple: int | None = None, +) -> Tensor: + """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. + + Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple + structure tokens but share a single ``(asym_id, residue_index)`` key — + collapse them to one LM token per residue before running the LM (the LM + was trained on per-residue inputs, not per-atom), then scatter the + hidden states back to the per-token layout. + """ + B, L = input_ids.shape + device = input_ids.device + protein_mask = (mol_type == 0) & token_mask + + lm_input_list = [] + lm_lengths = [] + # Per-batch maps from (original protein-token index) to (LM input position). + expand_maps: list[Tensor] = [] + for b in range(B): + mask_b = protein_mask[b] + ids_b = input_ids[b][mask_b] + asym_b = asym_id[b][mask_b] + res_b = residue_index[b][mask_b] + + # Collapse: keep first token per (asym_id, residue_index) key, in + # input order. ``inverse`` maps each original protein-token to its + # collapsed residue index. + keys = torch.stack((asym_b, res_b), dim=1) + unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) + n_unique = unique_keys.size(0) + token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) + first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) + first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) + ordered = torch.argsort(first_pos) + first_pos_ordered = first_pos[ordered] + ids_collapsed = ids_b[first_pos_ordered] + asym_collapsed = asym_b[first_pos_ordered] + remap = torch.empty_like(ordered) + remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) + inverse_ordered = remap[inverse] + + chain_ids = asym_collapsed.unique(sorted=True) + # [BOS] chain1 [EOS BOS] chain2 ... [EOS] + parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] + # Per-chain LM positions accumulate; track them for the expand map. + per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) + cursor = 1 # position 0 is the leading BOS + for i, cid in enumerate(chain_ids): + in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] + parts.append(ids_collapsed[in_chain]) + per_token_lm_pos[in_chain] = torch.arange( + cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long + ) + cursor += in_chain.shape[0] + if i < len(chain_ids) - 1: + parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) + cursor += 2 # EOS + BOS + parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) + lm_seq = torch.cat(parts) + lm_input_list.append(lm_seq) + lm_lengths.append(lm_seq.shape[0]) + + # Original protein-token position → LM input position. + prot_pos_b = mask_b.nonzero(as_tuple=True)[0] + expand_map = torch.full((L,), -1, device=device, dtype=torch.long) + expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] + expand_maps.append(expand_map) + + # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. + max_len = max(lm_lengths) + if pad_to_multiple is not None and pad_to_multiple > 1: + max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple + lm_input_ids = torch.full( + (B, max_len), + 1, + device=device, + dtype=input_ids.dtype, # PAD=1 + ) + for b in range(B): + lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] + + # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). + sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 + sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 + + with torch.inference_mode(): + esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) + + # ESMC returns hidden states as the standard tuple of per-layer tensors; stack + # them into the single [n_layers+1, B, max_len, D] tensor the projection expects. + hs = torch.stack(esmc_out.hidden_states, dim=0) # [n_layers+1, B, max_len, D] + n_layers_plus_1, _, _, D = hs.shape + result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) + for b in range(B): + mb = protein_mask[b] + em = expand_maps[b][mb] # [n_protein_tokens] LM positions + # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] + gathered = hs[:, b, em, :].permute(1, 0, 2) + result[b, mb.nonzero(as_tuple=True)[0]] = gathered + + return result.detach() + + class ESMFold2Model(ESMFold2PreTrainedModel): """ESMFold2 — all-atom structure prediction with an ESMC PLM backbone. From af1a479d9a9495d9bbdcb5aded49e8dbfa3f732a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 18:49:49 +0100 Subject: [PATCH 42/70] More modular cleanup --- .../models/esmfold2/modular_esmfold2.py | 3174 +++++++++++++++++ 1 file changed, 3174 insertions(+) create mode 100644 src/transformers/models/esmfold2/modular_esmfold2.py diff --git a/src/transformers/models/esmfold2/modular_esmfold2.py b/src/transformers/models/esmfold2/modular_esmfold2.py new file mode 100644 index 000000000000..bfbd1ec1989e --- /dev/null +++ b/src/transformers/models/esmfold2/modular_esmfold2.py @@ -0,0 +1,3174 @@ +# Copyright 2026 Biohub. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""PyTorch ESMFold2 model — the standard released architecture. + +Quickstart:: + + from transformers import ESMFold2Model + + model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() + open("ubq.pdb", "w").write(model.infer_protein_as_pdb("MQIFVKTLTGKT...")) + +``infer_protein`` / ``infer_protein_as_pdb`` cover single-chain protein folding from a +sequence; multi-chain, ligand, or MSA inputs are supported by building the structural +feature tensors yourself and passing them directly to ``forward``. +""" + +from __future__ import annotations + +import math +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor +from torch.utils.checkpoint import checkpoint + +from ... import initialization as init +from ...integrations import use_kernel_forward_from_hub +from ...modeling_outputs import ModelOutput +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from ...utils import is_flash_attn_2_available +from ..auto import AutoModel + +# ESMFold2 reuses Llama's plain attention helpers verbatim. `eager_attention_forward` +# upcasts the softmax to fp32 (Llama's variant, NOT esm's no-upcast one) which the +# bit-exact bf16 backbone relies on, and pulls in `repeat_kv` as a dependency. The +# atom-track 3D RoPE (`apply_rotary_emb_3d`) reuses Llama's `rotate_half`. +from ..llama.modeling_llama import eager_attention_forward, rotate_half +from .configuration_esmfold2 import ESMFold2Config + + +if is_flash_attn_2_available(): + from flash_attn import flash_attn_func, flash_attn_varlen_func + from flash_attn.bert_padding import index_first_axis, pad_input + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +CHAR_VOCAB_SIZE = 64 +MAX_CHARS = 4 +XYZ_DIMS = 3 +MAX_ATOMIC_NUMBER = 128 + +# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 +ATOM_FEATURE_DIM = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS + + +NUM_RES_TYPES = 33 + +_EPS = 1e-5 + +# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory +# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; +# chunk=64 leaves headroom for the largest foldbench targets). Override via +# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for +# short L but OOM-prone past ~600). +_DEFAULT_CHUNK_SIZE = 64 + + +# =========================================================================== +# Atom-token utilities +# =========================================================================== + + +def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: + """Broadcast per-token features to per-atom features using gather. + + Args: + token_features: [B, L, d] + atom_to_token_idx: [B, A] int64 + + Returns: + [B, A, d] + """ + idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) + return torch.gather(token_features, 1, idx) + + +def scatter_atom_to_token( + atom_features: Tensor, + atom_to_token_idx: Tensor, + n_tokens: int, + atom_mask: Tensor | None = None, +) -> Tensor: + """Aggregate per-atom features to per-token features (mean). + + Args: + atom_features: [B, A, d] + atom_to_token_idx: [B, A] int64 + n_tokens: L + atom_mask: [B, A] bool + + Returns: + [B, L, d] + """ + B, A, d = atom_features.shape + n_out = n_tokens + idx = atom_to_token_idx + if atom_mask is not None: + idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) + n_out = n_tokens + 1 + idx_expanded = idx.unsqueeze(-1).expand(B, A, d) + out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) + out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) + return out[:, :n_tokens, :] + + +def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: + """Gather representative atom coordinates for each token. + + Args: + coords: [B, A, 3] + rep_atom_idx: [B, L] int64 + + Returns: + [B, L, 3] + """ + idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) + return torch.gather(coords, 1, idx) + + +def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: + """Compute local atom index within each token (vectorised). + + Atoms belonging to the same token are contiguous, so this computes a + running count that resets at each token boundary. + + Args: + atom_to_token: [B, A] flat index mapping each atom to its token. + + Returns: + [B, A] tensor with values in [0, max_atoms_per_token - 1]. + """ + same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) + ones = torch.ones_like(atom_to_token) + cumsum = torch.cumsum(ones, dim=-1) + group_start = cumsum.masked_fill(same_as_prev, 0) + group_start = torch.cummax(group_start, dim=-1).values + return cumsum - group_start + + +def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: + """Expected value of a categorical distribution over evenly-spaced bins. + + Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. + + Args: + logits: [..., n_bins] + start: left boundary + end: right boundary + + Returns: + [...] expected value + """ + n_bins = logits.shape[-1] + edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) + v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] + return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) + + +# =========================================================================== +# fp32-compute LayerNorm +# =========================================================================== + + +class ESMFold2LayerNorm(nn.LayerNorm): + """LayerNorm that always computes in fp32, with its weight stored at the model dtype. + + ESMFold2 runs in the loaded dtype (bf16 for inference) but its LayerNorms must match + the reference's fp32 norm *compute* (the fork upcasts via autocast). Keeping the weight + at the model dtype means ``from_pretrained(dtype=bf16)`` rounds it to bf16 exactly like + the reference's bf16-trained norm weights, while this ``forward`` upcasts to fp32 for + the computation and casts the result back — so no post-load weight fix-up is needed + (an fp32 load keeps full-precision weights and an fp32 compute, both no-ops here). + """ + + def forward(self, x: Tensor) -> Tensor: + weight = self.weight.float() if self.weight is not None else None + bias = self.bias.float() if self.bias is not None else None + return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) + + +# =========================================================================== +# ESMFold2TransitionLayer (used in ESMFold2DiffusionConditioning) +# =========================================================================== + + +class ESMFold2TransitionLayer(nn.Module): + """ESMFold2SwiGLU transition: norm -> a_proj, b_proj -> silu(a)*b -> out_proj.""" + + def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: + super().__init__() + hidden = n * d_model + self.norm = ESMFold2LayerNorm(d_model, eps=eps) + self.a_proj = nn.Linear(d_model, hidden, bias=False) + self.b_proj = nn.Linear(d_model, hidden, bias=False) + self.out_proj = nn.Linear(hidden, d_model, bias=False) + + def forward(self, x: Tensor) -> Tensor: + x = self.norm(x) + a = self.a_proj(x) + b = self.b_proj(x) + return self.out_proj(F.silu(a) * b) + + +# =========================================================================== +# ESMFold2AdaptiveLayerNorm (used in ESMFold2DiffusionTransformer) +# =========================================================================== + + +class ESMFold2AdaptiveLayerNorm(nn.Module): + """Adaptive layer normalization (adaLN-Zero).""" + + def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: + super().__init__() + self.d_model = d_model + self.d_cond = d_cond + self.eps = eps + self.s_scale = nn.Parameter(torch.empty(d_cond)) # ones-init in _init_weights + self.s_gate = nn.Linear(d_cond, d_model, bias=True) + self.s_shift = nn.Linear(d_cond, d_model, bias=False) + + def forward(self, a: Tensor, s: Tensor) -> Tensor: + a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) + s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) + # gate/shift in bf16 (matches the reference's autocast); a_norm is fp32 so the + # affine promotes to fp32, then downcast for the next op. + gate = torch.sigmoid(self.s_gate(s_norm)) + shift = self.s_shift(s_norm) + return (gate * a_norm + shift).to(a.dtype) + + +# =========================================================================== +# ESMFold2FourierEmbedding +# =========================================================================== + + +class ESMFold2FourierEmbedding(nn.Module): + """Fourier embedding: cos(2*pi*(t*w + b)).""" + + w: Tensor + b: Tensor + + def __init__(self, c: int) -> None: + super().__init__() + self.c = c + self.register_buffer("w", torch.randn(c)) + self.register_buffer("b", torch.randn(c)) + + def forward(self, t_hat: Tensor) -> Tensor: + # w/b are kept fp32 (ESMFold2Model._keep_in_fp32_modules_strict), so the random + # frequencies/phases — and the cos embedding — are computed at full precision. + t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) + return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) + + +# =========================================================================== +# ESMFold2SwiGLU / ESMFold2SwiGLUMLP +# =========================================================================== + + +class ESMFold2SwiGLU(nn.Module): + """ESMFold2SwiGLU with packed w12 and output w3.""" + + def __init__( + self, + in_features: int, + hidden_features: int, + out_features: int | None = None, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) + self.w3 = nn.Linear(hidden_features, out_features, bias=bias) + self.hidden_features = hidden_features + + def forward(self, x: Tensor) -> Tensor: + x12 = self.w12(x) + x1, x2 = x12.split(self.hidden_features, dim=-1) + hidden = F.silu(x1) * x2 + return self.w3(hidden) + + +class ESMFold2SwiGLUMLP(ESMFold2SwiGLU): + """ESMFold2SwiGLU MLP with packed weights, no bias.""" + + def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: + hidden = expansion_ratio * d_model + super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) + + +# =========================================================================== +# SWA Atom Attention components +# =========================================================================== + + +def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: + """Apply RoPE with batch-dependent cos/sin. + + Args: + x: [B, L, H, D] + cos: [B, L, D/2] + sin: [B, L, D/2] + """ + ro_dim = cos.shape[-1] * 2 + cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) + sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) + return torch.cat( + [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], + dim=-1, + ) + + +@torch.compiler.disable +def build_3d_rope( + ref_pos: Tensor, + ref_space_uid: Tensor, + head_dim: int, + n_spatial_per_axis: int = 4, + n_uid_pairs: int = 2, + spatial_base_freq: float = 10000.0, + uid_base_freq: float = 10.0, +) -> tuple[Tensor, Tensor]: + """Build cos/sin for 3D RoPE + UID RoPE.""" + device = ref_pos.device + B, N = ref_pos.shape[:2] + half_dim = head_dim // 2 + n_spatial_total = 3 * n_spatial_per_axis + + spatial_inv_freq = 1.0 / ( + spatial_base_freq + ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) + ) + uid_inv_freq = 1.0 / ( + uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) + ) + + pos_f32 = ref_pos.float() + spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) + spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) + + uid_f32 = ref_space_uid.float() + uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) + + n_active = n_spatial_total + n_uid_pairs + freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) + + if n_active < half_dim: + padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) + freqs = torch.cat([freqs, padding], dim=-1) + + cos = freqs.cos().to(torch.bfloat16) + sin = freqs.sin().to(torch.bfloat16) + return cos, sin + + +def qk_norm(x: Tensor) -> Tensor: + return F.rms_norm(x, (x.size(-1),)) + + +# =========================================================================== +# ESMFold2SwiGLUFFN (atom transformer blocks) +# =========================================================================== + + +class ESMFold2SwiGLUFFN(nn.Module): + """ESMFold2SwiGLU FFN with rounded hidden size for hardware alignment.""" + + def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: + super().__init__() + hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 + self.w_up = nn.Linear(d_model, 2 * hidden_size, bias=False) + self.w_down = nn.Linear(hidden_size, d_model, bias=False) + + def forward(self, x: Tensor) -> Tensor: + x = x.to(self.w_up.weight.dtype) + x1, x2 = self.w_up(x).chunk(2, dim=-1) + return self.w_down(F.silu(x1) * x2) + + +# =========================================================================== +# ESMFold2SWA3DRoPEAttention +# =========================================================================== + + +class ESMFold2SWA3DRoPEAttention(nn.Module): + """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. + + The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention + interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), + with the sliding window expressed as an additive attention mask. The custom + flash-attention path (native bidirectional ``window_size``, plus varlen for + packed inputs) is kept as an opt-in backend, selected when + ``_attn_implementation == "flash_attention_2"``. ``config`` is attached by + the parent ``ESMFold2Model`` after construction; it is ``None`` (→ ``sdpa``) + when the module is used standalone. + """ + + def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: + super().__init__() + self.config = None + self.n_heads = n_heads + self.head_dim = d_model // n_heads + self.scale = self.head_dim**-0.5 + self.half_window = half_window + # No grouped-query attention; identity repeat keeps the interface happy. + self.num_key_value_groups = 1 + # Bidirectional encoder: never let the sdpa/flash interface default to + # causal masking when attention_mask happens to be None. + self.is_causal = False + + self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) + self.out_proj = nn.Linear(d_model, d_model, bias=False) + self.gate_proj = nn.Linear(d_model, d_model, bias=False) + + def forward(self, x: Tensor, attention_params: tuple) -> Tensor: + B, N = x.shape[:2] + cos, sin = attention_params[0], attention_params[1] + + x_input = x + qkv = self.Wqkv(x) + qkv = qkv.view(B, N, 3, self.n_heads, self.head_dim).permute(2, 0, 1, 3, 4) + q, k, v = qkv.unbind(0) + q, k = qk_norm(q), qk_norm(k) + + q = apply_rotary_emb_3d(q, cos, sin) + k = apply_rotary_emb_3d(k, cos, sin) + + input_dtype = q.dtype + if q.dtype not in (torch.float16, torch.bfloat16): + q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() + + attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" + use_flash = attn_impl == "flash_attention_2" and is_flash_attn_2_available() + + if use_flash and len(attention_params) > 2: + indices, cu_seqlens, max_seqlen = ( + attention_params[2], + attention_params[3], + attention_params[4], + ) + q_unpad = index_first_axis(q.reshape(-1, self.n_heads, self.head_dim), indices) + k_unpad = index_first_axis(k.reshape(-1, self.n_heads, self.head_dim), indices) + v_unpad = index_first_axis(v.reshape(-1, self.n_heads, self.head_dim), indices) + out_unpad = flash_attn_varlen_func( + q_unpad, + k_unpad, + v_unpad, + cu_seqlens, + cu_seqlens, + max_seqlen, + max_seqlen, + softmax_scale=self.scale, + window_size=(self.half_window, self.half_window), + ) + out = pad_input(out_unpad, indices, B, N) + elif use_flash: + out = flash_attn_func( + q, + k, + v, + softmax_scale=self.scale, + window_size=(self.half_window, self.half_window), + ) + else: + if len(attention_params) > 2: + valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) + valid[attention_params[2]] = True + valid = valid.view(B, N) + else: + valid = torch.ones(B, N, dtype=torch.bool, device=q.device) + rank = torch.cumsum(valid, dim=1) - 1 + within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window + allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) + allowed |= torch.eye(N, dtype=torch.bool, device=q.device) + # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. + attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) + attn_mask = attn_mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(q.dtype).min) + + attention_interface: Callable = eager_attention_forward + if attn_impl != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) + out, _ = attention_interface( + self, + q.transpose(1, 2), + k.transpose(1, 2), + v.transpose(1, 2), + attn_mask, + dropout=0.0, + scaling=self.scale, + ) + out = out * valid.unsqueeze(-1).unsqueeze(-1) + + out = out.to(input_dtype).reshape(B, N, -1) + out = out * torch.sigmoid(self.gate_proj(x_input)) + return self.out_proj(out) + + +# =========================================================================== +# ESMFold2SWAAtomBlock, ESMFold2SWAAtomTransformer +# =========================================================================== + + +def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: + return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift + + +def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: + return x + gate * y + + +class ESMFold2SWAAtomBlock(nn.Module): + """adaLN-Zero + SWA attention + ESMFold2SwiGLU FFN. + + Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight + """ + + def __init__( + self, + d_atom: int, + n_heads: int, + half_window: int = 64, + expansion_ratio: int = 2, + ) -> None: + super().__init__() + # adaln-Zero gate; zero-init lives in ESMFold2PreTrainedModel._init_weights. + self.adaln_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_atom, 6 * d_atom, bias=False)) + + self.attn = ESMFold2SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) + self.ffn = ESMFold2SwiGLUFFN(d_atom, expansion_ratio) + + def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: + mod = self.adaln_modulation(c_l) + if mod.dim() == 2: + mod = mod.unsqueeze(1) + shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) + + attn_input = _rms_adaln(x, scale_a, shift_a) + attn_out = self.attn(attn_input, attention_params) + x = _gated_residual(x, gate_a, attn_out) + + ffn_input = _rms_adaln(x, scale_f, shift_f) + ffn_out = self.ffn(ffn_input) + x = _gated_residual(x, gate_f, ffn_out) + return x + + +class ESMFold2SWAAtomTransformer(nn.Module): + """Stack of SWAAtomBlocks.""" + + def __init__( + self, + d_atom: int = 128, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.swa_window_size = swa_window_size + self.head_dim = d_atom // n_heads + self.spatial_rope_base_frequency = spatial_rope_base_frequency + self.n_spatial_rope_pairs_per_axis = n_spatial_rope_pairs_per_axis + self.n_uid_rope_pairs = n_uid_rope_pairs + self.uid_rope_base_frequency = uid_rope_base_frequency + + self.blocks = nn.ModuleList( + [ + ESMFold2SWAAtomBlock( + d_atom=d_atom, + n_heads=n_heads, + half_window=swa_window_size // 2, + expansion_ratio=expansion_ratio, + ) + for _ in range(n_blocks) + ] + ) + + def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: + return build_3d_rope( + ref_pos=ref_pos, + ref_space_uid=ref_space_uid, + head_dim=self.head_dim, + n_spatial_per_axis=self.n_spatial_rope_pairs_per_axis, + n_uid_pairs=self.n_uid_rope_pairs, + spatial_base_freq=self.spatial_rope_base_frequency, + uid_base_freq=self.uid_rope_base_frequency, + ) + + def forward( + self, + q_l: Tensor, + c_l: Tensor, + attention_params: tuple, + return_intermediates: bool = False, + ) -> Tensor | tuple[Tensor, list[Tensor]]: + intermediates: list[Tensor] = [] + for block in self.blocks: + q_l = block(q_l, c_l, attention_params) + if return_intermediates: + intermediates.append(q_l) + if return_intermediates: + return q_l, intermediates + return q_l + + +# =========================================================================== +# ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) +# =========================================================================== + + +class ESMFold2AtomEncoder(nn.Module): + """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. + + Args: + d_atom: atom hidden dim + d_token: token dim for atom_to_token aggregation + n_blocks, n_heads, swa_window_size, expansion_ratio: transformer params + structure_prediction: if True, creates coords_linear and uses full d_token + spatial_rope_base_frequency, n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs, uid_rope_base_frequency: 3D RoPE config + """ + + def __init__( + self, + d_atom: int = 128, + d_token: int = 768, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + structure_prediction: bool = True, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.d_atom = d_atom + self.d_token = d_token + self.structure_prediction = structure_prediction + + self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) + self.atom_norm = ESMFold2LayerNorm(d_atom) + + if structure_prediction: + self.coords_linear = nn.Linear(6, d_atom, bias=False) + + self.atom_transformer = ESMFold2SWAAtomTransformer( + d_atom=d_atom, + n_blocks=n_blocks, + n_heads=n_heads, + swa_window_size=swa_window_size, + expansion_ratio=expansion_ratio, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # Output aggregation: d_token for structure prediction, d_token//2 for inputs + out_dim = d_token if structure_prediction else d_token // 2 + self.atom_to_token_linear = nn.Linear(d_atom, out_dim, bias=False) + + def forward( + self, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + r_l: Tensor | None = None, + pred_r1: Tensor | None = None, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + inference_cache: dict | None = None, + ) -> tuple[Tensor, Tensor, Tensor, tuple, list[Tensor]]: + """Returns (a, q, c, attention_params, intermediates). + + ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, + attention indices, n_tokens) across diffusion steps. + """ + B, N = ref_pos.shape[:2] + + layer_cache = None + if inference_cache is not None: + layer_cache = inference_cache.setdefault("atomencoder", {}) + + if layer_cache is None or len(layer_cache) == 0: + atom_feats = torch.cat( + [ + ref_pos, + ref_charge.unsqueeze(-1), + atom_attention_mask.unsqueeze(-1), + ref_element, + ref_atom_name_chars.reshape(B, N, MAX_CHARS * CHAR_VOCAB_SIZE), + ], + dim=-1, + ) + c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( + self.atom_linear.weight.dtype + ) + cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) + cos = cos.repeat_interleave(num_diffusion_samples, 0) + sin = sin.repeat_interleave(num_diffusion_samples, 0) + mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) + seqlens = mask_exp.sum(dim=-1, dtype=torch.int32) + indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() + max_seqlen = int(seqlens.max().item()) + cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) + attention_params = (cos, sin, indices, cu_seqlens, max_seqlen) + n_tokens = int(atom_to_token.max().item()) + 1 + if layer_cache is not None: + layer_cache["c_base"] = c_base + layer_cache["attention_params"] = attention_params + layer_cache["mask_exp"] = mask_exp + layer_cache["n_tokens"] = n_tokens + layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + else: + c_base = layer_cache["c_base"] + attention_params = layer_cache["attention_params"] + mask_exp = layer_cache["mask_exp"] + n_tokens = layer_cache["n_tokens"] + + c = c_base + + q = c + + if self.structure_prediction and r_l is not None: + q = q.repeat_interleave(num_diffusion_samples, 0) + if pred_r1 is None: + pred_r1 = torch.zeros_like(r_l) + r_input = torch.cat([r_l, pred_r1], dim=-1) + r_to_q = self.coords_linear(r_input.to(self.coords_linear.weight.dtype)) + q = q + r_to_q + + c = c.repeat_interleave(num_diffusion_samples, 0) + + result = self.atom_transformer( + q_l=q, + c_l=c, + attention_params=attention_params, + return_intermediates=return_intermediates, + ) + if return_intermediates: + q, intermediates = result + else: + q = result + intermediates = [] + + q_to_a = F.relu(self.atom_to_token_linear(q)) + if layer_cache is not None and "atom_to_token_exp" in layer_cache: + atom_to_token_exp = layer_cache["atom_to_token_exp"] + else: + atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) + + return a, q, c, attention_params, intermediates + + +# =========================================================================== +# ESMFold2AtomDecoder +# =========================================================================== + + +class ESMFold2AtomDecoder(nn.Module): + """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear.""" + + def __init__( + self, + d_atom: int = 128, + d_token: int = 768, + n_blocks: int = 3, + n_heads: int = 4, + swa_window_size: int = 128, + expansion_ratio: int = 2, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) + + self.atom_transformer = ESMFold2SWAAtomTransformer( + d_atom=d_atom, + n_blocks=n_blocks, + n_heads=n_heads, + swa_window_size=swa_window_size, + expansion_ratio=expansion_ratio, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + self.norm = ESMFold2LayerNorm(d_atom) + self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) + + def forward( + self, + a_i: Tensor, + q_l: Tensor, + c_l: Tensor, + p_lm: tuple, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + ) -> tuple[Tensor, list[Tensor]]: + """Returns (r_update, intermediates).""" + atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + a_to_q = self.token_to_atom_linear(a_i) + a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) + q_l = q_l + a_to_q + + result = self.atom_transformer( + q_l=q_l, + c_l=c_l, + attention_params=p_lm, + return_intermediates=return_intermediates, + ) + if return_intermediates: + q_l, intermediates = result + else: + q_l = result + intermediates = [] + + r_l = self.output_linear(self.norm(q_l)) + return r_l, intermediates + + +# =========================================================================== +# ESMFold2AttentionPairBias (ESMFold2DiffusionTransformer attention block) +# =========================================================================== + + +class ESMFold2AttentionPairBias(nn.Module): + """Gated multi-head attention with pair bias conditioning.""" + + def __init__( + self, + d_model: int, + d_pair: int, + num_heads: int, + d_cond: int | None = None, + use_conditioning: bool = True, + ) -> None: + super().__init__() + self.d_model = d_model + self.num_heads = num_heads + self.head_dim = d_model // num_heads + self.scale = self.head_dim**-0.5 + d_cond = d_cond or d_model + + if use_conditioning: + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. + self.out_gate = nn.Linear(d_cond, d_model, bias=True) + else: + self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) + + self.q_proj = nn.Linear(d_model, d_model, bias=True) + self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) + self.g_proj = nn.Linear(d_model, d_model, bias=False) + self.out_proj = nn.Linear(d_model, d_model, bias=False) + + if d_pair > 0: + self.pair_norm = ESMFold2LayerNorm(d_pair, eps=1e-5) + self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) + + def compute_pair_bias(self, z: Tensor, bsz: int, num_diffusion_samples: int = 1) -> Tensor: + """Project the (normed) pair representation to per-head attention biases. + + Depends only on ``z`` and this block's fixed weights, so it is invariant + across diffusion sampling steps — the sampler computes it once and reuses + it (see ``ESMFold2DiffusionTransformer.forward``). Bit-identical to computing it + inline every step. + """ + if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: + z = z.repeat_interleave(num_diffusion_samples, dim=0) + if z.dim() == 4: + return self.pair_bias_proj(self.pair_norm(z)) + return z.unsqueeze(-1) + + def forward( + self, + a: Tensor, + s: Tensor | None, + z: Tensor, + attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + pair_bias: Tensor | None = None, + ) -> Tensor: + bsz, n_queries, d_model = a.shape + + if s is not None: + x = self.adaln(a, s) + else: + x = self.pre_norm(a) + + n_keys = x.shape[1] + q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) + kv = self.kv_proj(x) + k, v = kv.chunk(2, dim=-1) + k = k.view(bsz, n_keys, self.num_heads, self.head_dim) + v = v.view(bsz, n_keys, self.num_heads, self.head_dim) + + if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: + attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) + + # Standard attention with pair bias + g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) + + logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale + + # ``pair_bias`` is step-invariant; the diffusion sampler precomputes and + # caches it across steps. Compute inline when not supplied (e.g. uncached). + if pair_bias is None: + pair_bias = self.compute_pair_bias(z, bsz, num_diffusion_samples) + logits = logits + pair_bias.to(dtype=logits.dtype) + + if attention_mask is not None: + min_val = torch.finfo(logits.dtype).min + mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) + logits = logits + mask_bias.to(dtype=logits.dtype) + + attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) + ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) + ctx = g * ctx + out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) + + if s is not None: + out = torch.sigmoid(self.out_gate(s)) * out + return out + + +# =========================================================================== +# ESMFold2ConditionedTransitionBlock +# =========================================================================== + + +class ESMFold2ConditionedTransitionBlock(nn.Module): + """Conditioned ESMFold2SwiGLU transition with adaptive layer norm.""" + + def __init__( + self, + d_model: int, + d_cond: int | None = None, + transition_multiplier: int = 2, + use_conditioning: bool = True, + ) -> None: + super().__init__() + d_cond = d_cond or d_model + hidden = transition_multiplier * d_model + + if use_conditioning: + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. + self.output_gate = nn.Linear(d_cond, d_model, bias=True) + else: + self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) + + self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) + self.lin_out = nn.Linear(hidden, d_model, bias=False) + + def forward(self, a: Tensor, s: Tensor | None) -> Tensor: + if s is not None: + x = self.adaln(a, s) + else: + x = self.pre_norm(a) + + swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) + b = F.silu(swish_a) * swish_b + out = self.lin_out(b) + + if s is not None: + out = torch.sigmoid(self.output_gate(s)) * out + return out + + +# =========================================================================== +# ESMFold2DiffusionTransformer (token transformer) +# =========================================================================== + + +class ESMFold2DiffusionTransformer(nn.Module): + """Diffusion denoising transformer with attention pair bias.""" + + def __init__( + self, + d_model: int, + d_pair: int, + num_heads: int, + num_blocks: int, + d_cond: int | None = None, + transition_multiplier: int = 2, + use_conditioning: bool = True, + ) -> None: + super().__init__() + d_cond = d_cond or d_model + + self.attn_blocks = nn.ModuleList( + [ + ESMFold2AttentionPairBias( + d_model=d_model, + d_pair=d_pair, + num_heads=num_heads, + d_cond=d_cond, + use_conditioning=use_conditioning, + ) + for _ in range(num_blocks) + ] + ) + self.transition_blocks = nn.ModuleList( + [ + ESMFold2ConditionedTransitionBlock( + d_model=d_model, + d_cond=d_cond, + transition_multiplier=transition_multiplier, + use_conditioning=use_conditioning, + ) + for _ in range(num_blocks) + ] + ) + + def forward( + self, + a: Tensor, + s: Tensor | None, + z: Tensor, + attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + return_intermediates: bool = False, + inference_cache: dict | None = None, + ) -> tuple[Tensor, list[Tensor]]: + intermediates: list[Tensor] = [] + x = a + bsz = a.shape[0] + # Each block's pair bias depends only on the (step-invariant) conditioning + # pair ``z`` and fixed weights, so compute it once per block and reuse it + # across every diffusion sampling step. Bit-identical to recomputing it + # each step; the cache lives in the sampler's per-fold ``inference_cache``. + bias_cache = None if inference_cache is None else inference_cache.setdefault("token_pair_bias", {}) + for i, (attn, transition) in enumerate(zip(self.attn_blocks, self.transition_blocks)): + if bias_cache is None: + pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) + elif i in bias_cache: + pair_bias = bias_cache[i] + else: + pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) + bias_cache[i] = pair_bias + x = x + attn( + x, + s, + z, + attention_mask=attention_mask, + num_diffusion_samples=num_diffusion_samples, + pair_bias=pair_bias, + ) + x = x + transition(x, s) + if return_intermediates: + intermediates.append(x) + return x, intermediates + + +# =========================================================================== +# ESMFold2DiffusionConditioning +# =========================================================================== + + +class ESMFold2DiffusionConditioning(nn.Module): + """Conditions pair and single representations on noise timestep.""" + + def __init__( + self, + c_z: int = 256, + c_s: int = 768, + c_s_inputs: int = 451, + sigma_data: float = 16.0, + fourier_dim: int = 256, + transition_multiplier: int = 2, + layer_norm_eps: float = 1e-5, + ) -> None: + super().__init__() + self.sigma_data = float(sigma_data) + self.c_z = c_z + self.c_s = c_s + self.c_s_inputs = c_s_inputs + + self.z_input_norm = ESMFold2LayerNorm(2 * c_z, eps=layer_norm_eps) + self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) + self.z_transitions = nn.ModuleList( + [ESMFold2TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] + ) + + self.s_input_norm = ESMFold2LayerNorm(c_s_inputs, eps=layer_norm_eps) + self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) + self.fourier = ESMFold2FourierEmbedding(fourier_dim) + self.noise_norm = ESMFold2LayerNorm(fourier_dim, eps=layer_norm_eps) + self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) + self.s_transitions = nn.ModuleList( + [ESMFold2TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] + ) + + def forward( + self, + t_hat: Tensor, + s_inputs: Tensor, + z_trunk: Tensor, + relative_position_encoding: Tensor, + sigma_data: float | None = None, + num_diffusion_samples: int = 1, + inference_cache: dict[str, Tensor] | None = None, + ) -> tuple[Tensor, Tensor]: + sigma = self.sigma_data if sigma_data is None else float(sigma_data) + base_batch = z_trunk.shape[0] + target_batch = base_batch * num_diffusion_samples + + # z conditioning (cached across diffusion steps — independent of t_hat) + if inference_cache is not None and "z" in inference_cache: + z = inference_cache["z"] + else: + z_rel = relative_position_encoding.to(dtype=torch.float32) + z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) + # The relpos/coords conditioning is fp32; z_input_norm keeps it fp32, + # then we hand off to z_proj in the model's compute dtype. + z = self.z_proj(self.z_input_norm(z).to(self.z_proj.weight.dtype)) + for block in self.z_transitions: + z = z + block(z) + if inference_cache is not None: + inference_cache["z"] = z + + # s conditioning + s_inputs_eff = s_inputs + if s_inputs_eff.shape[0] != target_batch: + s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) + + s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype)) + + # Noise embedding + t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) + if t.numel() == 1: + t = t.expand(target_batch) + elif t.shape[0] != target_batch: + t = t.repeat_interleave(num_diffusion_samples, 0) + t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) + n = self.fourier(t_noise) + n = self.noise_proj(self.noise_norm(n.float()).to(self.noise_proj.weight.dtype)) + s = s + n.unsqueeze(1) + + for block in self.s_transitions: + s = s + block(s) + + return s, z + + +# =========================================================================== +# ESMFold2DiffusionModule +# =========================================================================== + + +class ESMFold2DiffusionModule(nn.Module): + """Diffusion denoising module for structure prediction.""" + + def __init__( + self, + c_atom: int = 128, + c_token: int = 768, + c_z: int = 256, + c_s_inputs: int = 451, + sigma_data: float = 16.0, + fourier_dim: int = 256, + atom_num_blocks: int = 3, + atom_num_heads: int = 4, + token_num_blocks: int = 12, + token_num_heads: int = 16, + transition_multiplier: int = 2, + swa_window_size: int = 128, + spatial_rope_base_frequency: float = 20.0, + n_spatial_rope_pairs_per_axis: int = 2, + n_uid_rope_pairs: int = 10, + uid_rope_base_frequency: float = 10000.0, + ) -> None: + super().__init__() + self.sigma_data = float(sigma_data) + + self.conditioning = ESMFold2DiffusionConditioning( + c_z=c_z, + c_s=c_token, # conditioning s output is c_token + c_s_inputs=c_s_inputs, + sigma_data=sigma_data, + fourier_dim=fourier_dim, + transition_multiplier=transition_multiplier, + ) + + # Atom encoder (structure_prediction=True, with coords_linear) + self.atom_encoder = ESMFold2AtomEncoder( + d_atom=c_atom, + d_token=c_token, + n_blocks=atom_num_blocks, + n_heads=atom_num_heads, + swa_window_size=swa_window_size, + expansion_ratio=2, + structure_prediction=True, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # Atom decoder + self.atom_decoder = ESMFold2AtomDecoder( + d_atom=c_atom, + d_token=c_token, + n_blocks=atom_num_blocks, + n_heads=atom_num_heads, + swa_window_size=swa_window_size, + expansion_ratio=2, + spatial_rope_base_frequency=spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=n_uid_rope_pairs, + uid_rope_base_frequency=uid_rope_base_frequency, + ) + + # zero-init lives in ESMFold2PreTrainedModel._init_weights. + self.s_to_token = nn.Linear(c_token, c_token, bias=False) + + # Token transformer (ESMFold2DiffusionTransformer with pair bias) + self.token_transformer = ESMFold2DiffusionTransformer( + d_model=c_token, + d_pair=c_z, + num_heads=token_num_heads, + num_blocks=token_num_blocks, + d_cond=c_token, + transition_multiplier=transition_multiplier, + use_conditioning=True, + ) + + self.s_step_norm = ESMFold2LayerNorm(c_token) + self.token_norm = ESMFold2LayerNorm(c_token) + + def forward( + self, + x_noisy: Tensor, + t_hat: Tensor, + ref_pos: Tensor, + ref_charge: Tensor, + ref_mask: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + tok_idx: Tensor, + s_inputs: Tensor, + z_trunk: Tensor, + relative_position_encoding: Tensor, + asym_id: Tensor, + residue_index: Tensor, + entity_id: Tensor, + token_index: Tensor, + sym_id: Tensor, + sigma_data: float | None = None, + token_attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + return_atom_repr: bool = False, + inference_cache: dict[str, Tensor] | None = None, + ) -> dict[str, Tensor | None]: + bsz = x_noisy.shape[0] + sigma = self.sigma_data if sigma_data is None else float(sigma_data) + t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape(-1) + if t.numel() == 1: + t = t.expand(bsz) + + # Step 1: conditioning (pair z is cached across diffusion steps) + s, z = self.conditioning( + t_hat=t, + s_inputs=s_inputs, + z_trunk=z_trunk, + relative_position_encoding=relative_position_encoding, + sigma_data=sigma, + num_diffusion_samples=num_diffusion_samples, + inference_cache=inference_cache, + ) + + # Step 2: normalize noisy coords + denom = torch.sqrt(t * t + sigma * sigma) + r_noisy = x_noisy / denom[:, None, None] + + # Step 3: atom encoder + a, q_skip, c_skip, p_skip, enc_intermediates = self.atom_encoder( + ref_pos=ref_pos, + atom_attention_mask=ref_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=tok_idx, + r_l=r_noisy, + num_diffusion_samples=num_diffusion_samples, + return_intermediates=return_atom_repr, + inference_cache=inference_cache, + ) + + # Step 4: add conditioned s + a = a + self.s_to_token(self.s_step_norm(s)) + + # Step 5: token transformer (pair bias is cached across steps via inference_cache) + a, _ = self.token_transformer( + a, + s, + z, + attention_mask=token_attention_mask, + num_diffusion_samples=num_diffusion_samples, + inference_cache=inference_cache, + ) + + # Step 6: token norm + a = self.token_norm(a) + + # Step 7: atom decoder + r_update, dec_intermediates = self.atom_decoder( + a_i=a, + q_l=q_skip, + c_l=c_skip, + p_lm=p_skip, + atom_to_token=tok_idx, + atom_attention_mask=ref_mask, + num_diffusion_samples=num_diffusion_samples, + return_intermediates=return_atom_repr, + ) + + # Step 8: compute denoised output + sigma2 = sigma * sigma + t2 = t * t + out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy + out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update + + # Collect atom intermediates from encoder + decoder + atom_intermediates: Tensor | None = None + if return_atom_repr: + all_ints = enc_intermediates + dec_intermediates + if all_ints: + atom_intermediates = torch.stack(all_ints, dim=2) + + return { + "x_denoised": out, + "atom_intermediates": atom_intermediates, + } + + +# =========================================================================== +# ESMFold2DiffusionStructureHead +# =========================================================================== + + +class ESMFold2DiffusionStructureHead(nn.Module): + """Wrapper around ESMFold2DiffusionModule with diffusion sampling.""" + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + dm = config.structure_head.diffusion_module + swa_cfg = config.inputs.atom_encoder + sh = config.structure_head + + self.diffusion_module = ESMFold2DiffusionModule( + c_atom=dm.c_atom, + c_token=dm.c_token, + c_z=dm.c_z, + c_s_inputs=dm.c_s_inputs, + sigma_data=dm.sigma_data, + fourier_dim=dm.fourier_dim, + atom_num_blocks=dm.atom_num_blocks, + atom_num_heads=dm.atom_num_heads, + token_num_blocks=dm.token_num_blocks, + token_num_heads=dm.token_num_heads, + transition_multiplier=dm.transition_multiplier, + swa_window_size=swa_cfg.swa_window_size, + spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, + uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, + ) + + # Sampling hyperparameters + self.sigma_data = dm.sigma_data + self.gamma_0 = sh.gamma_0 + self.gamma_min = sh.gamma_min + self.noise_scale = sh.noise_scale + self.step_scale = sh.step_scale + self.inference_s_max = sh.inference_s_max + self.inference_s_min = sh.inference_s_min + self.inference_p = sh.inference_p + self.inference_num_steps = sh.inference_num_steps + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def inference_noise_schedule(self, num_steps: int | None = None, device: torch.device | None = None) -> Tensor: + """Karras power-law noise schedule.""" + steps = self.inference_num_steps if num_steps is None else int(num_steps) + if steps == 1: + return torch.tensor( + [self.inference_s_max * self.sigma_data, 0.0], + device=device, + dtype=torch.float32, + ) + p = float(self.inference_p) + inv_p = 1.0 / p + k = torch.arange(steps, device=device, dtype=torch.float32) + base = self.inference_s_max**inv_p + (k / (steps - 1)) * ( + self.inference_s_min**inv_p - self.inference_s_max**inv_p + ) + schedule = self.sigma_data * base.pow(p) + return F.pad(schedule, (0, 1), value=0.0) + + @staticmethod + def _random_rotations(n: int, dtype: torch.dtype, device: torch.device) -> Tensor: + q = torch.randn((n, 4), dtype=dtype, device=device) + scale = torch.sqrt((q * q).sum(dim=1)) + signs = torch.where(q[:, 0] < 0, -scale, scale) + q = q / signs[:, None] + r, i, j, k = torch.unbind(q, dim=-1) + two_s = 2.0 / (q * q).sum(dim=-1) + return torch.stack( + ( + 1 - two_s * (j * j + k * k), + two_s * (i * j - k * r), + two_s * (i * k + j * r), + two_s * (i * j + k * r), + 1 - two_s * (i * i + k * k), + two_s * (j * k - i * r), + two_s * (i * k - j * r), + two_s * (j * k + i * r), + 1 - two_s * (i * i + j * j), + ), + dim=-1, + ).reshape(n, 3, 3) + + def _center_random_augmentation( + self, x: Tensor, atom_mask: Tensor, second_coords: Tensor | None = None + ) -> tuple[Tensor, Tensor | None]: + """Algorithm 19: center + random rotation + translation.""" + bsz = x.shape[0] + mask = atom_mask.unsqueeze(-1) # [B, A, 1] + denom = mask.sum(dim=1, keepdim=True).clamp(min=1) + mean = (x * mask).sum(dim=1, keepdim=True) / denom + x = x - mean + if second_coords is not None: + second_coords = second_coords - mean + + r = self._random_rotations(bsz, x.dtype, x.device) + x = torch.einsum("bmd,bds->bms", x, r) + if second_coords is not None: + second_coords = torch.einsum("bmd,bds->bms", second_coords, r) + + t = torch.randn_like(x[:, 0:1, :]) + x = x + t + if second_coords is not None: + second_coords = second_coords + t + return x, second_coords + + @staticmethod + def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> Tensor: + """Kabsch alignment: align x to x_gt with weights w.""" + w = (mask * w).unsqueeze(-1) # [B, N, 1] + denom = w.sum(dim=-2, keepdim=True).clamp(min=1e-8) + mu = (x * w).sum(dim=-2, keepdim=True) / denom + mu_gt = (x_gt * w).sum(dim=-2, keepdim=True) / denom + x_c = x - mu + xgt_c = x_gt - mu_gt + H = torch.einsum("bni,bnj->bij", w * xgt_c, x_c) + H32 = H.float() + U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) + det = torch.linalg.det(U @ Vh) + ones = torch.ones_like(det) + R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to(H.dtype) + return x_c @ R.transpose(-1, -2) + mu_gt + + # ------------------------------------------------------------------ + # Sampling + # ------------------------------------------------------------------ + + @torch.inference_mode() + def sample( + self, + z_trunk: Tensor, + s_inputs: Tensor, + relative_position_encoding: Tensor, + ref_pos: Tensor, + ref_charge: Tensor, + ref_mask: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + tok_idx: Tensor, + asym_id: Tensor, + residue_index: Tensor, + entity_id: Tensor, + token_index: Tensor, + sym_id: Tensor, + token_attention_mask: Tensor | None = None, + num_diffusion_samples: int = 1, + num_sampling_steps: int | None = None, + max_inference_sigma: float | None = 256.0, + noise_scale: float | None = None, + step_scale: float | None = None, + return_atom_repr: bool = False, + use_inference_cache: bool = True, + denoising_early_exit_rmsd: float | None = None, + ) -> dict[str, Tensor | None]: + """Diffusion sampling (Algorithm 18). + + ``num_sampling_steps`` is the number of denoising steps actually run. + When ``max_inference_sigma`` is set, the Karras schedule built with + ``num_sampling_steps`` entries would lose its high-σ tail to the cap, + so we inflate the underlying schedule length here to land back at the + requested step count post-truncation. + """ + n_atoms = tok_idx.shape[1] + device = s_inputs.device + target_batch = s_inputs.shape[0] * num_diffusion_samples + + inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None + + steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) + + schedule = self.inference_noise_schedule(steps, device) + if max_inference_sigma is not None: + schedule = schedule[schedule <= float(max_inference_sigma)] + schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) + + lam = self.noise_scale if noise_scale is None else float(noise_scale) + eta = self.step_scale if step_scale is None else float(step_scale) + + x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) + atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() + + gammas = torch.where( + schedule > self.gamma_min, + torch.full_like(schedule, self.gamma_0), + torch.zeros_like(schedule), + ) + + x_denoised_prev: Tensor | None = None + diff_atom_intermediates: Tensor | None = None + + step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) + num_steps = len(step_pairs) + + for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): + x, x_denoised_prev = self._center_random_augmentation(x, atom_mask, second_coords=x_denoised_prev) + + sigma_tm_val = float(sigma_tm.item()) + t_hat_val = sigma_tm_val * (1.0 + float(gamma.item())) + eps_std = lam * max(t_hat_val**2 - sigma_tm_val**2, 0.0) ** 0.5 + x_noisy = x + eps_std * torch.randn_like(x) + + is_last_step = step_idx == num_steps - 1 + request_atom_repr = return_atom_repr and (is_last_step or denoising_early_exit_rmsd is not None) + + dm_out = self.diffusion_module( + x_noisy=x_noisy, + t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), + ref_pos=ref_pos, + ref_charge=ref_charge, + ref_mask=ref_mask, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + ref_space_uid=ref_space_uid, + tok_idx=tok_idx, + s_inputs=s_inputs, + z_trunk=z_trunk, + relative_position_encoding=relative_position_encoding, + asym_id=asym_id, + residue_index=residue_index, + entity_id=entity_id, + token_index=token_index, + sym_id=sym_id, + token_attention_mask=token_attention_mask, + num_diffusion_samples=num_diffusion_samples, + return_atom_repr=request_atom_repr, + inference_cache=inference_cache, + ) + + x_denoised = dm_out["x_denoised"] + if request_atom_repr: + diff_atom_intermediates = dm_out.get("atom_intermediates") + + # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts + # to fp32 internally for the SVD/det. + x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) + x_noisy = x_noisy.to(dtype=x_denoised.dtype) + + # ODE/SDE step + sigma_t_val = float(sigma_t.item()) + denoised_over_sigma = (x_noisy - x_denoised) / t_hat_val + x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma + + # Denoising early-exit: stop when consecutive predictions converge + if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: + aligned = self._weighted_rigid_align( + x_denoised_prev.float(), + x_denoised.float(), + atom_mask, + atom_mask, + ) + diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) + per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() + if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: + x = x_denoised + x_denoised_prev = x_denoised + break + + x_denoised_prev = x_denoised + + result: dict[str, Tensor | None] = { + "sample_atom_coords": x, + } + if return_atom_repr: + result["diff_atom_intermediates"] = diff_atom_intermediates + return result + + +# =========================================================================== +# ESMFold2RowAttentionPooling +# =========================================================================== + + +class ESMFold2RowAttentionPooling(nn.Module): + """Row-wise attention pooling: attn_proj, out_proj.""" + + def __init__(self, d_pair: int, d_single: int) -> None: + super().__init__() + self.attn_proj = nn.Linear(d_pair, 1, bias=False) + self.out_proj = nn.Linear(d_pair, d_single, bias=False) + + def forward(self, z: Tensor, mask: Tensor) -> Tensor: + scores = self.attn_proj(z).squeeze(-1) + mask_bias = torch.where( + mask[:, None, :].bool(), + torch.zeros_like(scores), + torch.full_like(scores, -1e9), + ) + scores = scores + mask_bias + weights = F.softmax(scores, dim=-1, dtype=torch.float32).to(scores.dtype) + pooled = torch.einsum("bnm,bnmd->bnd", weights, z) + return self.out_proj(pooled) + + +# =========================================================================== +# ESMFold2InputsEmbedder +# =========================================================================== + + +class ESMFold2InputsEmbedder(nn.Module): + """Embeds input features including atom-level encoding via SWA attention.""" + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + swa_cfg = config.inputs.atom_encoder + + self.atom_attention_encoder = ESMFold2AtomEncoder( + d_atom=swa_cfg.d_atom, + d_token=swa_cfg.d_token, + n_blocks=swa_cfg.n_blocks, + n_heads=swa_cfg.n_heads, + swa_window_size=swa_cfg.swa_window_size, + expansion_ratio=swa_cfg.expansion_ratio, + structure_prediction=False, # no coords_linear + spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, + n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, + n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, + uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, + ) + + def forward( + self, + aatype: Tensor, + profile: Tensor, + deletion_mean: Tensor, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + ) -> Tensor: + """Embed inputs into per-token features. + + Returns: + [B, L, d_inputs] concatenation of atom encoding, aatype, profile, + and deletion_mean. + """ + a, _q, _c, _attn_params, _intermediates = self.atom_attention_encoder( + ref_pos=ref_pos, + atom_attention_mask=atom_attention_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=atom_to_token, + ) + # The continuous input features are fp32; fold them into the atom + # encoding's (compute) dtype so the single representation is one dtype. + dtype = a.dtype + return torch.cat( + [a, aatype.to(dtype), profile.to(dtype), deletion_mean.unsqueeze(-1).to(dtype)], + dim=-1, + ) + + +# =========================================================================== +# ESMFold2ResIdxAsymIdSymIdEntityIdEncoding (trunk relative position) +# =========================================================================== + + +class ESMFold2ResIdxAsymIdSymIdEntityIdEncoding(nn.Module): + """embed.weight [d_pair, n_features] where n_features = 2*(2*r_bins+2) + 1 + (2*c_bins+2). + + For default r_bins=32, c_bins=2: 2*66 + 1 + 6 = 139. + """ + + def __init__( + self, + n_relative_residx_bins: int = 32, + n_relative_chain_bins: int = 2, + d_pair: int = 256, + ) -> None: + super().__init__() + self.n_relative_residx_bins = n_relative_residx_bins + self.n_relative_chain_bins = n_relative_chain_bins + self.d_pair = d_pair + + n_feats_residue = 2 * n_relative_residx_bins + 2 + n_feats_token = 2 * n_relative_residx_bins + 2 + n_feats_chain = 2 * n_relative_chain_bins + 2 + n_feats_same_entity = 1 + total_feats = n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity + self.embed = nn.Linear(total_feats, d_pair, bias=False) + + def forward( + self, + residue_index: Tensor, + asym_id: Tensor, + sym_id: Tensor, + entity_id: Tensor, + token_index: Tensor, + ) -> Tensor: + bij_same_chain = asym_id.unsqueeze(2) == asym_id.unsqueeze(1) + bij_same_residue = residue_index.unsqueeze(2) == residue_index.unsqueeze(1) + bij_same_entity = entity_id.unsqueeze(2) == entity_id.unsqueeze(1) + + dij_residue = residue_index.unsqueeze(2) - residue_index.unsqueeze(1) + dij_residue = torch.clip( + dij_residue + self.n_relative_residx_bins, + 0, + 2 * self.n_relative_residx_bins, + ) + dij_residue = torch.where(bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1) + aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) + + dij_token = torch.clip( + token_index.unsqueeze(2) - token_index.unsqueeze(1) + self.n_relative_residx_bins, + 0, + 2 * self.n_relative_residx_bins, + ) + dij_token = torch.where( + bij_same_chain & bij_same_residue, + dij_token, + 2 * self.n_relative_residx_bins + 1, + ) + aij_rel_token = F.one_hot(dij_token, 2 * self.n_relative_residx_bins + 2) + + dij_chain = torch.clip( + sym_id.unsqueeze(2) - sym_id.unsqueeze(1) + self.n_relative_chain_bins, + 0, + 2 * self.n_relative_chain_bins, + ) + dij_chain = torch.where(bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain) + aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) + + feats = torch.cat( + [ + aij_rel_pos.float(), + aij_rel_token.float(), + bij_same_entity.float().unsqueeze(-1), + aij_rel_chain.float(), + ], + dim=-1, + ) + + return self.embed(feats.to(self.embed.weight.dtype)) + + +# =========================================================================== +# ESMFold2SingleToPair (for ESMFold2LanguageModelShim) +# =========================================================================== + + +class ESMFold2SingleToPair(nn.Module): + """downproject, output_mlp (Sequential of Linear, GELU, Linear).""" + + def __init__(self, input_dim: int, downproject_dim: int, output_dim: int) -> None: + super().__init__() + self.downproject = nn.Linear(input_dim, downproject_dim) + self.output_mlp = nn.Sequential( + nn.Linear(2 * downproject_dim, output_dim), + nn.GELU(), + nn.Linear(output_dim, output_dim), + ) + + def forward(self, x: Tensor) -> Tensor: + x = self.downproject(x) + x = torch.cat( + [(x.unsqueeze(2) * x.unsqueeze(1)), (x.unsqueeze(2) - x.unsqueeze(1))], + dim=3, + ) + return self.output_mlp(x) + + +# =========================================================================== +# ESMFold2LanguageModelShim +# =========================================================================== + + +class ESMFold2LanguageModelShim(nn.Module): + """Shim holding the trainable projection weights for LM integration. + + Contains: + - base_z_combine: nn.Parameter [num_layers+1] + - base_z_linear: Sequential(ESMFold2LayerNorm(d_model), Linear(d_model, d_z, bias=False)) + - base_z_mlp: Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + """ + + def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: + super().__init__() + + self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + self.base_z_linear = nn.Sequential(ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) + self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) + + def forward(self, hidden_states: Tensor) -> Tensor: + """Project pre-computed ESMC hidden states to pair representation. + + Args: + hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. + + Returns: + [B, L, L, d_pair] pair representation. + """ + # The ESMC backbone may be loaded at a different precision than the trunk + # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. + hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) + # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. + normed = self.base_z_linear[0](hidden_states) + lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] + weights = self.base_z_combine.softmax(0) # [81] + lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] + # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. + pair = self.base_z_mlp[0](lm_z) + lm_z = self.base_z_mlp[1](pair) # [B, L, L, d_z] + return lm_z + + +# =========================================================================== +# ESMFold2 — language-model backbone helpers +# =========================================================================== + + +def compute_lm_hidden_states( + esmc: nn.Module, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + token_mask: Tensor, + pad_to_multiple: int | None = None, +) -> Tensor: + """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. + + Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple + structure tokens but share a single ``(asym_id, residue_index)`` key — + collapse them to one LM token per residue before running the LM (the LM + was trained on per-residue inputs, not per-atom), then scatter the + hidden states back to the per-token layout. + """ + B, L = input_ids.shape + device = input_ids.device + protein_mask = (mol_type == 0) & token_mask + + lm_input_list = [] + lm_lengths = [] + # Per-batch maps from (original protein-token index) to (LM input position). + expand_maps: list[Tensor] = [] + for b in range(B): + mask_b = protein_mask[b] + ids_b = input_ids[b][mask_b] + asym_b = asym_id[b][mask_b] + res_b = residue_index[b][mask_b] + + # Collapse: keep first token per (asym_id, residue_index) key, in + # input order. ``inverse`` maps each original protein-token to its + # collapsed residue index. + keys = torch.stack((asym_b, res_b), dim=1) + unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) + n_unique = unique_keys.size(0) + token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) + first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) + first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) + ordered = torch.argsort(first_pos) + first_pos_ordered = first_pos[ordered] + ids_collapsed = ids_b[first_pos_ordered] + asym_collapsed = asym_b[first_pos_ordered] + remap = torch.empty_like(ordered) + remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) + inverse_ordered = remap[inverse] + + chain_ids = asym_collapsed.unique(sorted=True) + # [BOS] chain1 [EOS BOS] chain2 ... [EOS] + parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] + # Per-chain LM positions accumulate; track them for the expand map. + per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) + cursor = 1 # position 0 is the leading BOS + for i, cid in enumerate(chain_ids): + in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] + parts.append(ids_collapsed[in_chain]) + per_token_lm_pos[in_chain] = torch.arange( + cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long + ) + cursor += in_chain.shape[0] + if i < len(chain_ids) - 1: + parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) + cursor += 2 # EOS + BOS + parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) + lm_seq = torch.cat(parts) + lm_input_list.append(lm_seq) + lm_lengths.append(lm_seq.shape[0]) + + # Original protein-token position → LM input position. + prot_pos_b = mask_b.nonzero(as_tuple=True)[0] + expand_map = torch.full((L,), -1, device=device, dtype=torch.long) + expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] + expand_maps.append(expand_map) + + # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. + max_len = max(lm_lengths) + if pad_to_multiple is not None and pad_to_multiple > 1: + max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple + lm_input_ids = torch.full( + (B, max_len), + 1, + device=device, + dtype=input_ids.dtype, # PAD=1 + ) + for b in range(B): + lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] + + # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). + sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 + sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 + + with torch.inference_mode(): + esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) + + # ESMC returns hidden states as the standard tuple of per-layer tensors; stack + # them into the single [n_layers+1, B, max_len, D] tensor the projection expects. + hs = torch.stack(esmc_out.hidden_states, dim=0) # [n_layers+1, B, max_len, D] + n_layers_plus_1, _, _, D = hs.shape + result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) + for b in range(B): + mb = protein_mask[b] + em = expand_maps[b][mb] # [n_protein_tokens] LM positions + # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] + gathered = hs[:, b, em, :].permute(1, 0, 2) + result[b, mb.nonzero(as_tuple=True)[0]] = gathered + + return result.detach() + + +# =========================================================================== +# ESMFold2TriangleMultiplicativeUpdate +# =========================================================================== +@use_kernel_forward_from_hub("ESMFold2TriangleMultiplication") +class ESMFold2TriangleMultiplicativeBlock(nn.Module): + """Triangle multiplicative update block with gated signal routing. + + The O(N^3) triangular contraction below is the trunk's dominant cost. Loading + with ``ESMFold2Model.from_pretrained(..., device_map="cuda", use_kernels=True)`` + (CUDA + inference) swaps the whole block forward for a fused Triton kernel from + the Hub (see the ``hub_kernels`` mapping); the pure-PyTorch ``forward`` here stays + as the reference/fallback. The kernel reads this module's parameters + (``norm_start``/``norm_mix``/``proj_bundle``/``proj_emit``/``proj_gate``) and + matches ``forward``'s ``(pair_grid, visibility)`` signature, returning the + residual-free delta. + """ + + _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} + _VALID_FLOWS = ("outgoing", "incoming") + + def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None: + super().__init__() + if flow not in self._FLOW_TO_EINSUM: + raise ValueError(f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}.") + + self.input_channels = input_channels + self.latent_channels = latent_channels + self.flow = flow + self._einsum_equation = self._FLOW_TO_EINSUM[flow] + self.norm_start = ESMFold2LayerNorm(self.input_channels, eps=_EPS) + self.norm_mix = ESMFold2LayerNorm(self.latent_channels, eps=_EPS) + self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) + self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) + self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) + + # Default chunked for memory on long sequences; tests override with + # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 + # parity checks. + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: + return torch.einsum(self._einsum_equation, left_stream, right_stream) + + def _triangular_contract_chunked(self, left_stream: Tensor, right_stream: Tensor, chunk_size: int) -> Tensor: + """Compute the triangular einsum in chunks along the output i-dimension.""" + L = left_stream.shape[1] if self.flow == "outgoing" else left_stream.shape[2] + chunks = [] + for start in range(0, L, chunk_size): + end = min(start + chunk_size, L) + if self.flow == "outgoing": + chunk = torch.einsum(self._einsum_equation, left_stream[:, start:end], right_stream) + else: + chunk = torch.einsum(self._einsum_equation, left_stream[:, :, start:end], right_stream) + chunks.append(chunk) + return torch.cat(chunks, dim=1) + + def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor: + if visibility is None: + visibility = pair_grid.new_ones(pair_grid.shape[:-1]) + + normalized_grid = self.norm_start(pair_grid) + bundled = self.proj_bundle(normalized_grid) + signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) + # Gates and the O(N^3) contraction run in the activation dtype (bf16). This + # matches the reference: under its autocast the einsum is downcast to bf16, + # and the fused Triton kernel likewise contracts in bf16 — the dtype the + # checkpoint was trained with. Keeping these in fp32 was a (marginal) + # precision *up* that diverges from training and is slower on the trunk's + # dominant op. ``norm_start``/``norm_mix`` stay fp32. A no-op in fp32. + routed = signal * torch.sigmoid(gate_logits) + routed = routed * visibility.unsqueeze(-1) + + left_stream, right_stream = routed.chunk(2, dim=-1) + if self._chunk_size is not None: + contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) + else: + contracted = self._triangular_contract(left_stream, right_stream) + mixed = self.proj_emit(self.norm_mix(contracted.float()).to(self.proj_emit.weight.dtype)) + output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) + return mixed * output_gate + + +class ESMFold2TriangleMultiplicativeUpdate(nn.Module): + """Thin wrapper exposing the triangular mixer with explicit orientation (v3).""" + + def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: + super().__init__() + flow = "outgoing" if _outgoing else "incoming" + self._engine = ESMFold2TriangleMultiplicativeBlock(input_channels=dim, latent_channels=dim, flow=flow) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._engine.set_chunk_size(chunk_size) + + def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: + return self._engine(z, visibility=mask) + + +# =========================================================================== +# ESMFold2FoldingTrunk: ESMFold2Transition, ESMFold2PairUpdateBlock, ESMFold2FoldingTrunk +# =========================================================================== + + +class ESMFold2Transition(nn.Module): + """LayerNorm + ESMFold2SwiGLU feed-forward residual block, chunked along the token axis.""" + + def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: + super().__init__() + self.norm = ESMFold2LayerNorm(d_model) + self.ffn = ESMFold2SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) + # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. + self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def forward(self, x: Tensor) -> Tensor: + if self._chunk_size is None or x.shape[1] <= self._chunk_size: + return x + self.ffn(self.norm(x)) + out_list: list[Tensor] = [] + for s in range(0, x.shape[1], self._chunk_size): + e = min(s + self._chunk_size, x.shape[1]) + sl = x[:, s:e] + out_list.append(sl + self.ffn(self.norm(sl))) + return torch.cat(out_list, dim=1) + + +class ESMFold2PairUpdateBlock(nn.Module): + """tri_mul_out, tri_mul_in, pair_transition.""" + + def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: + super().__init__() + self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=expansion_ratio) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.tri_mul_out.set_chunk_size(chunk_size) + self.tri_mul_in.set_chunk_size(chunk_size) + self.pair_transition.set_chunk_size(chunk_size) + + def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: + # HF model is inference-only, so the trained row-shared dropout (r=0) is a no-op. + pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) + pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) + pair = self.pair_transition(pair) + return pair + + +class ESMFold2FoldingTrunk(nn.Module): + """ModuleList of PairUpdateBlocks.""" + + def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4) -> None: + super().__init__() + self.blocks = nn.ModuleList( + [ESMFold2PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) for _ in range(n_layers)] + ) + + def set_chunk_size(self, chunk_size: int | None) -> None: + for block in self.blocks: + block.set_chunk_size(chunk_size) + + def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: + for block in self.blocks: + fn = partial(block, pair_attention_mask=pair_attention_mask) + if torch.is_grad_enabled(): + pair = checkpoint(fn, pair, use_reentrant=False) + else: + pair = fn(pair) + return pair + + +# =========================================================================== +# MSA Encoder +# =========================================================================== + + +class ESMFold2OuterProductMean(nn.Module): + """Outer-product mean: maps an MSA representation into a pair update. + + The order of the ``/ n_valid`` divide vs. the ``Wout`` projection is + selectable via ``divide_outer_before_proj`` because different ESMFold2 + checkpoints were trained with different orderings: + + * ``False`` (default): ``Wout(outer) / n_valid`` — the projection bias + is scaled by 1/n_valid alongside the outer product. + * ``True``: ``Wout(outer / n_valid)`` — the projection bias is added + unscaled, post-divide. + """ + + def __init__( + self, + d_msa: int, + d_hidden: int, + d_pair: int, + divide_outer_before_proj: bool = False, + ) -> None: + super().__init__() + self.d_hidden = d_hidden + self.divide_outer_before_proj = divide_outer_before_proj + self.norm = ESMFold2LayerNorm(d_msa) + self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) + self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) + # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. + self._chunk_size: int | None = None + + def set_chunk_size(self, chunk_size: int | None) -> None: + self._chunk_size = chunk_size + + def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: + m_norm = self.norm(m) + x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) + a, b = x.chunk(2, dim=-1) + mask_f = msa_attention_mask.to(a.dtype) + n_valid = (mask_f @ mask_f.transpose(-1, -2)).unsqueeze(-1).clamp(min=1.0) + if self._chunk_size is None: + outer = torch.einsum("bimc,bjmd->bijcd", a, b).flatten(-2) + if self.divide_outer_before_proj: + return self.Wout(outer / n_valid) + return self.Wout(outer) / n_valid + # Chunk along the left (i) axis so the peak einsum intermediate is + # [B, chunk, L, c, d] instead of [B, L, L, c, d]. + L = a.shape[1] + out_chunks: list[Tensor] = [] + for s in range(0, L, self._chunk_size): + e = min(s + self._chunk_size, L) + outer_chunk = torch.einsum("bimc,bjmd->bijcd", a[:, s:e], b).flatten(-2) + if self.divide_outer_before_proj: + out_chunks.append(self.Wout(outer_chunk / n_valid[:, s:e])) + else: + out_chunks.append(self.Wout(outer_chunk) / n_valid[:, s:e]) + return torch.cat(out_chunks, dim=1) + + +class ESMFold2MSAPairWeightedAveraging(nn.Module): + """Pair-biased MSA row update (AF3 Supplement Algorithm 10).""" + + def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32) -> None: + super().__init__() + self.n_heads = n_heads + self.head_width = head_width + self.norm_single = ESMFold2LayerNorm(d_msa) + self.compute_bias = nn.Sequential(ESMFold2LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) + self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) + self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) + self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) + + def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor) -> Tensor: + """ + Args: + msa_repr: [B, L, M, d_msa] + pair_repr: [B, L, L, d_pair] + pair_attention_mask:[B, L, L] + Returns: + [B, L, M, d_msa] + """ + B, L, M, _ = msa_repr.shape + h, dh = self.n_heads, self.head_width + + msa_normed = self.norm_single(msa_repr) + bias = self.compute_bias[1](self.compute_bias[0](pair_repr)) # [B, L, L, n_heads] + bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) + attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j + + v = self.Wv(msa_normed).reshape(B, L, M, h, dh) + gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) + + output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) + return self.Wout(output.reshape(B, L, M, h * dh)) + + +_CONFIDENCE_EPS = 1e-6 +_NONPOLYMER_ID = 4 + + +@dataclass +class ESMFold2Output(ModelOutput): + """ + Output of [`ESMFold2Model`]. All confidence scores are on a 0-1 scale; per-sample tensors + have a leading `num_diffusion_samples` axis. + + Args: + distogram_logits (`torch.FloatTensor` of shape `(batch_size, num_tokens, num_tokens, distogram_bins)`): + Predicted distance-distribution logits over residue pairs (RNG-independent; no diffusion sampling). + sample_atom_coords (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, 3)`): + Predicted all-atom Cartesian coordinates for each diffusion sample. + plddt_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, num_plddt_bins)`): + Per-atom pLDDT bin logits. + plddt (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens)`): + Per-residue predicted lDDT confidence. + plddt_per_atom (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms)`): + Per-atom predicted lDDT confidence. + plddt_ca (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens)`): + Predicted lDDT at the representative (Cα) atom of each token. + complex_plddt (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Mean pLDDT over all atoms of the complex. + complex_iplddt (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Interface-weighted complex pLDDT. + pae_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens, num_pae_bins)`): + Predicted-aligned-error bin logits. + pae (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens)`): + Expected predicted aligned error (Å) for each residue pair. + pde_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens, num_pde_bins)`): + Predicted-distance-error bin logits. + pde (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens)`): + Expected predicted distance error (Å) for each residue pair. + resolved_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, 2)`): + Per-atom resolved/unresolved logits. + ptm (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Predicted TM-score for each sample. + iptm (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): + Predicted interface TM-score for each sample. + pair_chains_iptm (`torch.FloatTensor` of shape `(num_diffusion_samples, num_chains, num_chains)`): + Predicted interface TM-score for each ordered chain pair. + """ + + distogram_logits: Tensor | None = None + sample_atom_coords: Tensor | None = None + plddt_logits: Tensor | None = None + plddt: Tensor | None = None + plddt_per_atom: Tensor | None = None + plddt_ca: Tensor | None = None + complex_plddt: Tensor | None = None + complex_iplddt: Tensor | None = None + pae_logits: Tensor | None = None + pae: Tensor | None = None + pde_logits: Tensor | None = None + pde: Tensor | None = None + resolved_logits: Tensor | None = None + ptm: Tensor | None = None + iptm: Tensor | None = None + pair_chains_iptm: Tensor | None = None + + +class ESMFold2ConfidenceHead(nn.Module): + """Predicts pLDDT, PAE, PDE, resolved-atom probability and distogram bins.""" + + boundaries: Tensor + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__() + ch = config.confidence_head + d_single = config.d_single + d_pair = config.d_pair + d_inputs = config.inputs.d_inputs + + boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1) + self.register_buffer("boundaries", boundaries) + self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) + + self.s_norm = ESMFold2LayerNorm(d_single) + self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) + self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) + self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) + self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) + self.s_inputs_norm = ESMFold2LayerNorm(d_inputs) + self.z_norm = ESMFold2LayerNorm(d_pair) + + self.row_attention_pooling = ESMFold2RowAttentionPooling(d_pair=d_pair, d_single=d_single) + + pf = ch.folding_trunk + self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) + + # Heads. + self.plddt_ln = ESMFold2LayerNorm(d_single) + max_atoms_per_token = 23 + self.plddt_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)) + + self.pae_ln = ESMFold2LayerNorm(d_pair) + self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) + + self.pde_ln = ESMFold2LayerNorm(d_pair) + self.pde_head = nn.Linear(d_pair, ch.num_pde_bins, bias=False) + + self.resolved_ln = ESMFold2LayerNorm(d_single) + # 2 = resolved logits ([unresolved, resolved]). + self.resolved_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, 2)) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.folding_trunk.set_chunk_size(chunk_size) + + @staticmethod + def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor: + return x if num_diffusion_samples == 1 else x.repeat_interleave(num_diffusion_samples, 0) + + @staticmethod + def _flatten_sample_axis(x: Tensor) -> Tensor: + if x.ndim == 4: + b, mult, n, c = x.shape + return x.reshape(b * mult, n, c) + return x + + def forward( + self, + s_inputs: Tensor, + z: Tensor, + x_pred: Tensor, + distogram_atom_idx: Tensor, + token_attention_mask: Tensor, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + asym_id: Tensor, + mol_type: Tensor, + num_diffusion_samples: int = 1, + relative_position_encoding: Tensor | None = None, + token_bonds_encoding: Tensor | None = None, + ) -> dict[str, Tensor]: + s_inputs_normed = self.s_inputs_norm(s_inputs) + + z_base = self.z_norm(z) + if relative_position_encoding is not None: + z_base = z_base + relative_position_encoding + if token_bonds_encoding is not None: + z_base = z_base + token_bonds_encoding + z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2) + z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1) + z_base = z_base + self.s_to_z_prod_out( + self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] + ) + + pair = self._repeat_batch(z_base, num_diffusion_samples) + x_pred_flat = self._flatten_sample_axis(x_pred) + atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples) + atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples) + rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long() + mask = self._repeat_batch(token_attention_mask, num_diffusion_samples) + Bm = pair.shape[0] + + rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) + rep_distances = torch.cdist(rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist") + distogram_bins = (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() + pair = pair + self.dist_bin_pairwise_embed(distogram_bins) + + pair_mask = mask[:, :, None].float() * mask[:, None, :].float() + + # `pair` is fp32 here (built from the fp32 trunk output `z`); run the + # folding trunk in the model's compute dtype, then accumulate in fp32. + pair_delta = self.folding_trunk(pair.to(self.pae_head.weight.dtype), pair_attention_mask=pair_mask) + pair.add_(pair_delta.float()) + del pair_delta + # Accumulated in fp32; hand the downstream confidence heads the compute dtype. + pair = pair.to(self.pae_head.weight.dtype) + single = self.row_attention_pooling(pair, mask) + + atom_mask_f = atom_mask_m.float() + s_at_atoms = gather_token_to_atom(single, atom_to_token_m) + s_at_atoms_ln = self.plddt_ln(s_at_atoms) + + intra_idx = _compute_intra_token_idx(atom_to_token_m) + intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) + w_plddt = self.plddt_weight[intra_idx] + plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms_ln, w_plddt) + plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0) + + L = single.shape[1] + plddt_sum = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) + atom_count = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) + atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype) + plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t) + atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) + plddt = plddt_sum / atom_count.clamp(min=1e-6) + + complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (atom_mask_f.sum(dim=-1) + _CONFIDENCE_EPS) + + expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) + expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) + is_ligand = (expanded_type == _NONPOLYMER_ID).float() + inter_chain = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() + near_contact = (rep_distances < 8).float() + interface_per_token = (near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1)).amax(dim=-1) + iplddt_weight = torch.where( + is_ligand.bool(), + torch.full_like(interface_per_token, 2.0), + interface_per_token, + ) + iplddt_weight_atoms = gather_token_to_atom(iplddt_weight.unsqueeze(-1), atom_to_token_m).squeeze(-1) + atom_iplddt_w = atom_mask_f * iplddt_weight_atoms + complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (atom_iplddt_w.sum(dim=-1) + _CONFIDENCE_EPS) + + plddt_ca = plddt_per_atom.gather(1, rep_idx_m) + + # PAE + pae_logits = self.pae_head(self.pae_ln(pair)) + pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() + + # PDE + pde_logits = self.pde_head(self.pde_ln(pair)) + pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() + + # Resolved (per-atom binary). + s_at_atoms_res = self.resolved_ln(s_at_atoms) + w_res = self.resolved_weight[intra_idx] + resolved_logits = torch.einsum("...c,...cb->...b", s_at_atoms_res, w_res) + + # pTM / ipTM from pae_logits. + n_bins = pae_logits.shape[-1] + bin_width = 32.0 / n_bins + bin_centers = torch.arange(0.5 * bin_width, 32.0, bin_width, device=pae_logits.device) + mask_f = mask.float() + N_res = mask_f.sum(dim=-1, keepdim=True) + d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 + tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2) + pae_probs = F.softmax(pae_logits, dim=-1, dtype=torch.float32) + tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1) + + pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) + ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (pair_mask_2d.sum(dim=-1) + _CONFIDENCE_EPS) + ptm = ptm_per_row.max(dim=-1).values + + inter_chain_mask = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() * pair_mask_2d + iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (inter_chain_mask.sum(dim=-1) + _CONFIDENCE_EPS) + iptm = iptm_per_row.max(dim=-1).values + + max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 + n_chains = max_chain_id + 1 + pair_chains_iptm = torch.zeros(Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype) + for c1 in range(n_chains): + chain_c1 = (expanded_asym == c1).float() * mask_f + if chain_c1.sum() == 0: + continue + for c2 in range(n_chains): + chain_c2 = (expanded_asym == c2).float() * mask_f + pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) + denom = pair_m.sum(dim=(-1, -2)) + _CONFIDENCE_EPS + pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(dim=(-1, -2)) / denom + + return { + "plddt_logits": plddt_logits, + "plddt": plddt.detach(), + "plddt_per_atom": plddt_per_atom.detach(), + "plddt_ca": plddt_ca.detach(), + "complex_plddt": complex_plddt.detach(), + "complex_iplddt": complex_iplddt.detach(), + "pae_logits": pae_logits, + "pae": pae, + "pde_logits": pde_logits, + "pde": pde, + "resolved_logits": resolved_logits, + "ptm": ptm.detach(), + "iptm": iptm.detach(), + "pair_chains_iptm": pair_chains_iptm.detach(), + } + + +def _inverse_softplus(value: float) -> float: + return value + math.log(-math.expm1(-value)) + + +class ESMFold2PreTrainedModel(PreTrainedModel): + """Base class for ESMFold2 — declares the loading and weight-initialization behaviour.""" + + config_class = ESMFold2Config + base_model_prefix = "esmfold2" + main_input_name = "token_index" + _no_split_modules = [ + "ESMCLayer", + "ESMFold2PairUpdateBlock", + "ESMFold2AtomEncoder", + "ESMFold2AtomDecoder", + "ESMFold2DiffusionTransformer", + ] + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] + # The Fourier noise-embedding frequencies/phases are random Gaussian features whose + # precision drives the diffusion conditioning; keep them fp32 even under dtype=bf16. + _keep_in_fp32_modules_strict = ["fourier"] + _supports_sdpa = True + _supports_flash_attn = True + _supports_attention_backend = True + + def _init_weights(self, module): + # The base initializer handles Linear/Embedding/LayerNorm; below we (re)apply the + # few non-default inits the architecture needs (adaLN-Zero gates, identity/recurrent + # parcae params, zeroed projections). These live here, not in submodule __init__s, + # so they survive `post_init()` and are not wastefully run before weight loading. + # The `init.*` helpers are load-flag aware (they no-op on already-loaded weights). + super()._init_weights(module) + if isinstance(module, ESMFold2Model): + init.eye_(module.parcae_readout.weight) + init.eye_(module.parcae_b_cont) + init.zeros_(module.parcae_log_a) + parcae_delta_init = -math.log(math.sqrt(1.0 / 5.0)) + init.constant_(module.parcae_log_delta, _inverse_softplus(parcae_delta_init)) + elif isinstance(module, ESMFold2ConfidenceHead): + init.zeros_(module.plddt_weight) + init.zeros_(module.resolved_weight) + elif isinstance(module, ESMFold2AdaptiveLayerNorm): + init.ones_(module.s_scale) + elif isinstance(module, ESMFold2SWAAtomBlock): + init.zeros_(module.adaln_modulation[1].weight) + elif isinstance(module, ESMFold2AttentionPairBias): + if getattr(module, "out_gate", None) is not None: + init.zeros_(module.out_gate.weight) + init.constant_(module.out_gate.bias, -2.0) + elif isinstance(module, ESMFold2ConditionedTransitionBlock): + if getattr(module, "output_gate", None) is not None: + init.zeros_(module.output_gate.weight) + init.constant_(module.output_gate.bias, -2.0) + elif isinstance(module, ESMFold2DiffusionModule): + init.zeros_(module.s_to_token.weight) + elif isinstance(module, ESMFold2LanguageModelShim): + init.zeros_(module.base_z_combine) + + +class ESMFold2Model(ESMFold2PreTrainedModel): + """ESMFold2 — all-atom structure prediction with an ESMC PLM backbone. + + This is the standard released ESMFold2 architecture (uses a linear- + recurrent trunk, internally referred to as "parcae"). + + Forward kwargs that callers commonly override: + + * ``num_loops`` (default ``config.num_loops``): trunk refinement + loops. + * ``num_diffusion_samples`` (default ``config.num_diffusion_samples``): + parallel structure samples; the confidence head re-runs once per + sample, so memory scales linearly. Pass ``1`` for cheap inference. + * ``num_sampling_steps`` (default ``config.structure_head.inference_num_steps``): + diffusion ODE solver steps. Lower for speed, higher for quality. + + Memory / perf knobs: + + * ``model.set_chunk_size(int|None)``: caps L² ops (triangle / OPM / + pair transition) at this token-axis chunk. Default 64 — fits + L≈2k on an 80 GB GPU. Pass ``None`` for faster inference at L<600. + """ + + def __init__(self, config: ESMFold2Config) -> None: + super().__init__(config) + d_inputs = config.inputs.d_inputs + d_pair = config.d_pair + + self.inputs_embedder = ESMFold2InputsEmbedder(config) + self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) + self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False) + self.rel_pos = ESMFold2ResIdxAsymIdSymIdEntityIdEncoding( + n_relative_residx_bins=config.n_relative_residx_bins, + n_relative_chain_bins=config.n_relative_chain_bins, + d_pair=d_pair, + ) + self.token_bonds = nn.Linear(1, d_pair, bias=False) + self.language_model = ESMFold2LanguageModelShim( + d_z=d_pair, d_model=config.esmc_config.d_model, num_layers=config.esmc_config.n_layers + ) + # The ESMC backbone is a bundled submodule: built here with random weights + # (instant, no I/O), then populated by the parent's from_pretrained like any + # other composite sub-model (its weights live under ``esmc.*`` in the ESMFold2 + # checkpoint). It is frozen in effect — ``forward`` detaches its hidden states + # before they enter the trunk, so no gradient ever reaches it — and the bf16 + # autocast in ``_compute_lm_hidden_states`` reproduces the LM precision regime. + self.esmc = AutoModel.from_config(config.esmc_config) + + pf = config.folding_trunk + self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) + if config.lm_encoder.enabled: + self.lm_encoder: ESMFold2FoldingTrunk | None = ESMFold2FoldingTrunk( + n_layers=config.lm_encoder.n_layers, d_pair=d_pair, expansion_ratio=4 + ) + else: + self.lm_encoder = None + + # parcae linear-recurrence params (allocated here; initialized in _init_weights): + # log_a -> 0, log_delta -> a fixed decay constant, b_cont -> identity, readout -> identity. + self.parcae_input_norm = ESMFold2LayerNorm(d_pair) + self.parcae_log_a = nn.Parameter(torch.zeros(d_pair)) + self.parcae_log_delta = nn.Parameter(torch.empty(d_pair, dtype=torch.float32)) + self.parcae_b_cont = nn.Parameter(torch.empty(d_pair, d_pair)) + self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) + self.parcae_coda = ESMFold2FoldingTrunk(n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4) + + # Heads -------------------------------------------------------------- + self.structure_head = ESMFold2DiffusionStructureHead(config) + self.distogram_head = nn.Linear(d_pair, config.structure_head.distogram_bins, bias=True) + self.confidence_head = ESMFold2ConfidenceHead(config) + + msa_cfg = config.msa_encoder + self.msa_encoder = None + if msa_cfg.enabled: + self.msa_encoder = ESMFold2MSAEncoder( + d_msa=msa_cfg.d_msa, + d_pair=d_pair, + d_inputs=d_inputs, + d_hidden=msa_cfg.d_hidden, + n_layers=msa_cfg.n_layers, + n_heads_msa=msa_cfg.n_heads_msa, + msa_head_width=msa_cfg.msa_head_width, + ) + + # ESMFold2SWA3DRoPEAttention modules live deep in the atom encoders/decoders and + # are built from explicit dims, so give each a handle to the model config: + # their forward dispatches the plain-attention core through the v5 + # attention interface (config._attn_implementation), staying live under + # set_attn_implementation() since the config object is shared. + for module in self.modules(): + if isinstance(module, ESMFold2SWA3DRoPEAttention): + module.config = self.config + + self.post_init() + + def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = None) -> None: + """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" + import torch._dynamo + + torch._dynamo.config.cache_size_limit = 512 + torch._dynamo.config.accumulated_cache_size_limit = 512 + # capture_scalar_outputs avoids graph breaks at .item() in atom-attention path. + torch._dynamo.config.capture_scalar_outputs = True + + if dynamic is None: + dynamic = mode == "dynamic_seqlen" + kwargs: dict = {"dynamic": dynamic} + + compile_targets = ( + ESMFold2PairUpdateBlock, + ESMFold2DiffusionTransformer, + ESMFold2DiffusionModule, + ESMFold2MSAEncoderBlock, + ) + + def _maybe_compile(module: nn.Module) -> None: + if isinstance(module, compile_targets): + module.forward = torch.compile(module.forward, **kwargs) + + self.apply(_maybe_compile) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.folding_trunk.set_chunk_size(chunk_size) + if self.lm_encoder is not None: + self.lm_encoder.set_chunk_size(chunk_size) + self.parcae_coda.set_chunk_size(chunk_size) + self.confidence_head.set_chunk_size(chunk_size) + if self.msa_encoder is not None: + self.msa_encoder.set_chunk_size(chunk_size) + + def _compute_lm_hidden_states( + self, + input_ids: Tensor, + asym_id: Tensor, + residue_index: Tensor, + mol_type: Tensor, + tok_mask: Tensor, + ) -> Tensor: + # Run the frozen ESMC backbone exactly as the reference does: plain weights + # under a bf16 autocast (the fork's ``_lm_precision_context``). For a bf16 + # backbone this puts norms/softmax in fp32 and matmuls/rotary in bf16, + # reproducing the checkpoint's training regime bit-for-bit; disabled (a + # no-op) for an fp32 backbone. This autocast is scoped to the backbone only + # — the trunk stays dtype-honest. + use_amp = next(self.esmc.parameters()).dtype == torch.bfloat16 + with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=use_amp): + return compute_lm_hidden_states( + self.esmc, + input_ids, + asym_id, + residue_index, + mol_type, + tok_mask, + pad_to_multiple=None, + ) + + def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: + delta = F.softplus(self.parcae_log_delta) + a = torch.exp(-delta * torch.exp(self.parcae_log_a)) + b = delta[:, None] * self.parcae_b_cont + return a, b + + def _init_pair_state(self, ref: Tensor) -> Tensor: + std = math.sqrt(2.0 / (5.0 * ref.shape[-1])) + state = torch.empty_like(ref, dtype=torch.float32) + nn.init.trunc_normal_(state, mean=0.0, std=std, a=-3 * std, b=3 * std) + return state.to(dtype=ref.dtype) + + def _run_one_loop( + self, + z: Tensor, + z_init: Tensor, + lm_z: Tensor | None, + _msa_kwargs: dict | None, + pair_mask: Tensor, + a: Tensor, + b_mat: Tensor, + total_steps: int, + ) -> Tensor: + # Helper method (not inline) so per-iter locals free on return — + # otherwise leaks ~2 GB L²×c_z into distogram/sample scope. + # training=True forces dropout under eval(), matching the per-loop + # dropout strategy used at train time. + lm_cfg = self.config.lm_encoder + _per_loop_lm_dropout = ( + lm_z is not None + and getattr(lm_cfg, "per_loop_lm_dropout", False) + and getattr(lm_cfg, "lm_dropout", 0.0) > 0.0 + ) + _lm_dropout_p = getattr(lm_cfg, "lm_dropout", 0.0) + + for _ in range(total_steps): + if _per_loop_lm_dropout: + assert lm_z is not None # narrowed by _per_loop_lm_dropout + lm_z_i: Tensor | None = F.dropout(lm_z, p=_lm_dropout_p, training=True) + else: + lm_z_i = lm_z + + refined_lm_z: Tensor | None = None + if lm_z_i is not None and self.lm_encoder is not None: + refined_lm_z = self.lm_encoder(lm_z_i.to(z_init.dtype), pair_attention_mask=pair_mask) + + z_inject_pair = z_init + if lm_z_i is not None and self.lm_encoder is None: + z_inject_pair = z_inject_pair + lm_z_i.to(z_inject_pair.dtype) + + if self.msa_encoder is not None and _msa_kwargs is not None: + msa_pair = self.msa_encoder(x_pair=z_inject_pair, **_msa_kwargs).to(z_inject_pair.dtype) + z_inject_pair = msa_pair if self.config.msa_encoder_overwrite else (z_inject_pair + msa_pair) + + if refined_lm_z is not None: + z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) + + injected_pair = self.parcae_input_norm(z_inject_pair) + z = a * z + F.linear(injected_pair.to(z.dtype), b_mat) + z = self.folding_trunk(z, pair_attention_mask=pair_mask) + + return z + + def forward( + self, + token_index: Tensor, + residue_index: Tensor, + asym_id: Tensor, + sym_id: Tensor, + entity_id: Tensor, + mol_type: Tensor, + res_type: Tensor, + token_bonds: Tensor, + token_attention_mask: Tensor, + ref_pos: Tensor, + ref_element: Tensor, + ref_charge: Tensor, + ref_atom_name_chars: Tensor, + ref_space_uid: Tensor, + atom_attention_mask: Tensor, + atom_to_token: Tensor, + distogram_atom_idx: Tensor, + deletion_mean: Tensor | None = None, + msa: Tensor | None = None, + has_deletion: Tensor | None = None, + deletion_value: Tensor | None = None, + msa_attention_mask: Tensor | None = None, + input_ids: Tensor | None = None, + lm_hidden_states: Tensor | None = None, + num_loops: int | None = None, + num_diffusion_samples: int | None = None, + num_sampling_steps: int | None = None, + **kwargs, + ) -> ESMFold2Output: + tok_mask = token_attention_mask + atm_mask = atom_attention_mask + disto_idx = distogram_atom_idx + + n_loops: int = num_loops if num_loops is not None else self.config.num_loops + n_samples: int = ( + num_diffusion_samples if num_diffusion_samples is not None else self.config.num_diffusion_samples + ) + total_steps = max(1, n_loops + 1) + + if res_type.dim() == 2: + res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float() + res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() + else: + res_type_oh = res_type.float() + + if msa is not None: + msa_oh_profile = F.one_hot(msa.long(), num_classes=NUM_RES_TYPES).float() + if msa_attention_mask is not None: + mask_f = msa_attention_mask.float().unsqueeze(-1) + msa_oh_profile = msa_oh_profile * mask_f + valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) + profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) + else: + profile = msa_oh_profile.mean(dim=1) + else: + profile = res_type_oh + + if deletion_mean is None: + deletion_mean = torch.zeros(res_type.shape[0], res_type.shape[1], device=res_type.device) + + ref_element_oh = F.one_hot(ref_element.long(), num_classes=MAX_ATOMIC_NUMBER).float() + ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE).float() + # Bias-free downstream Linears require zeroed padding. + atm_mask_f = atm_mask.float() + ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) + ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(-1).unsqueeze(-1) + atom_to_token = atom_to_token * atm_mask.long() + + x_inputs = self.inputs_embedder( + aatype=res_type_oh, + profile=profile.float(), + deletion_mean=deletion_mean.float(), + ref_pos=ref_pos, + atom_attention_mask=atm_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element_oh, + ref_atom_name_chars=ref_atom_name_chars_oh, + atom_to_token=atom_to_token, + ) + + z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(x_inputs).unsqueeze(1) + + relative_position_encoding = self.rel_pos( + residue_index=residue_index, + asym_id=asym_id, + sym_id=sym_id, + entity_id=entity_id, + token_index=token_index, + ) + token_bonds_encoding = self.token_bonds(token_bonds.to(self.token_bonds.weight.dtype)) + z_init = z_init + relative_position_encoding + token_bonds_encoding + + if lm_hidden_states is None and input_ids is not None: + lm_hidden_states = self._compute_lm_hidden_states(input_ids, asym_id, residue_index, mol_type, tok_mask) + lm_z: Tensor | None = None + if lm_hidden_states is not None: + lm_z = self.language_model(lm_hidden_states.detach()) + del lm_hidden_states + + pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() + + z = self._init_pair_state(z_init) + + a, b = self._discretized_dynamics() + a = a.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) + b_mat = b.to(device=z.device, dtype=z.dtype) + + _msa_kwargs: dict | None = None + if self.msa_encoder is not None and msa is not None: + B_msa, M, L_msa = msa.shape + msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES).float() + msa_attn = ( + msa_attention_mask.permute(0, 2, 1).float() + if msa_attention_mask is not None + else tok_mask[:, :, None].expand(-1, -1, M).float() + ) + # Bias-free ESMFold2MSAEncoder.embed requires zeroed padding. + msa_oh = msa_oh * msa_attn.unsqueeze(-1) + hd = ( + has_deletion.permute(0, 2, 1).float() + if has_deletion is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + dv = ( + deletion_value.permute(0, 2, 1).float() + if deletion_value is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + _msa_kwargs = { + "x_inputs": x_inputs, + "msa_oh": msa_oh, + "has_deletion": hd, + "deletion_value": dv, + "msa_attention_mask": msa_attn, + } + + # Method call (not inline loop) frees per-iter L²×c_z locals. + z = self._run_one_loop( + z=z, + z_init=z_init, + lm_z=lm_z, + _msa_kwargs=_msa_kwargs, + pair_mask=pair_mask, + a=a, + b_mat=b_mat, + total_steps=total_steps, + ) + del z_init, lm_z, _msa_kwargs, a, b_mat + + z = self.parcae_readout(z) + z = self.parcae_coda(z, pair_attention_mask=pair_mask) + + z = z.float() + distogram_logits = self.distogram_head((z + z.transpose(-2, -3)).to(self.distogram_head.weight.dtype)) + + structure_output = self.structure_head.sample( + z_trunk=z, + s_inputs=x_inputs, + relative_position_encoding=relative_position_encoding, + ref_pos=ref_pos, + ref_charge=ref_charge, + ref_mask=atm_mask, + ref_element=ref_element_oh, + ref_atom_name_chars=ref_atom_name_chars_oh, + ref_space_uid=ref_space_uid, + tok_idx=atom_to_token, + asym_id=asym_id, + residue_index=residue_index, + entity_id=entity_id, + token_index=token_index, + sym_id=sym_id, + token_attention_mask=tok_mask, + num_diffusion_samples=n_samples, + num_sampling_steps=num_sampling_steps, + return_atom_repr=False, + denoising_early_exit_rmsd=None, + ) + + sample_coords = structure_output["sample_atom_coords"] + if sample_coords is None: + raise RuntimeError("The diffusion structure head did not return sampled coordinates.") + + confidence_output = self.confidence_head( + s_inputs=x_inputs.detach(), + z=z.detach().float(), + x_pred=sample_coords.detach(), + distogram_atom_idx=disto_idx, + token_attention_mask=tok_mask, + atom_to_token=atom_to_token, + atom_attention_mask=atm_mask, + asym_id=asym_id, + mol_type=mol_type, + num_diffusion_samples=n_samples, + relative_position_encoding=relative_position_encoding.detach(), + token_bonds_encoding=token_bonds_encoding.detach(), + ) + + return ESMFold2Output( + distogram_logits=distogram_logits, + sample_atom_coords=sample_coords, + **confidence_output, + ) + + @torch.no_grad() + def infer_protein(self, seq: str, **forward_kwargs) -> ESMFold2Output: + from .protein_utils import prepare_protein_features + + features = prepare_protein_features(seq) + features = {k: v.to(self.device) for k, v in features.items()} + return self(**features, **forward_kwargs) + + @torch.no_grad() + def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str: + from .protein_utils import output_to_pdb, prepare_protein_features + + features = prepare_protein_features(seq) + features = {k: v.to(self.device) for k, v in features.items()} + output = self(**features, **forward_kwargs) + return output_to_pdb(output, features) + + @staticmethod + def output_to_pdb(output: ESMFold2Output, features: dict[str, Tensor]) -> str: + """Render a PDB string from an [`ESMFold2Output`] and the input ``features`` it was produced from.""" + from .protein_utils import output_to_pdb as _output_to_pdb + + return _output_to_pdb(output, features) + + +class ESMFold2MSAEncoderBlock(nn.Module): + """One MSA encoder block: OPM into pair, MSA pair-weighted averaging, triangle update.""" + + def __init__( + self, + d_msa: int, + d_pair: int, + d_hidden: int, + n_heads_msa: int, + msa_head_width: int, + is_final_block: bool = False, + ) -> None: + super().__init__() + self.is_final_block = is_final_block + self.outer_product_mean = ESMFold2OuterProductMean(d_msa, d_hidden, d_pair) + if not is_final_block: + self.msa_pair_weighted_averaging = ESMFold2MSAPairWeightedAveraging( + d_msa, d_pair, n_heads_msa, msa_head_width + ) + self.msa_transition = ESMFold2Transition(d_msa, expansion_ratio=4) + self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) + self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=4) + + def set_chunk_size(self, chunk_size: int | None) -> None: + self.outer_product_mean.set_chunk_size(chunk_size) + self.tri_mul_out.set_chunk_size(chunk_size) + self.tri_mul_in.set_chunk_size(chunk_size) + if not self.is_final_block: + self.msa_transition.set_chunk_size(chunk_size) + self.pair_transition.set_chunk_size(chunk_size) + + def forward( + self, + m: Tensor, + pair: Tensor, + msa_attention_mask: Tensor, + pair_attention_mask: Tensor, + ) -> tuple[Tensor, Tensor]: + pair = pair + self.outer_product_mean(m, msa_attention_mask) + if not self.is_final_block: + m = m + self.msa_pair_weighted_averaging(m, pair, pair_attention_mask) + m = self.msa_transition(m) + pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) + pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) + pair = self.pair_transition(pair) + return m, pair + + +class ESMFold2MSAEncoder(nn.Module): + """Stack of [`ESMFold2MSAEncoderBlock`] layers that conditions the pair on an MSA.""" + + def __init__( + self, + d_msa: int, + d_pair: int, + d_inputs: int, + d_hidden: int = 32, + n_layers: int = 4, + n_heads_msa: int = 8, + msa_head_width: int = 16, + ) -> None: + super().__init__() + self.embed = nn.Linear(35, d_msa, bias=False) + self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) + self.blocks = nn.ModuleList( + [ + ESMFold2MSAEncoderBlock( + d_msa=d_msa, + d_pair=d_pair, + d_hidden=d_hidden, + n_heads_msa=n_heads_msa, + msa_head_width=msa_head_width, + is_final_block=(i == n_layers - 1), + ) + for i in range(n_layers) + ] + ) + + def set_chunk_size(self, chunk_size: int | None) -> None: + for block in self.blocks: + block.set_chunk_size(chunk_size) + + def forward( + self, + x_pair: Tensor, + x_inputs: Tensor, + msa_oh: Tensor, + has_deletion: Tensor, + deletion_value: Tensor, + msa_attention_mask: Tensor, + ) -> Tensor: + # All inputs are pre-transposed to [B, L, M, ...] before calling. + m_feat = torch.cat([msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1) + m = self.embed(m_feat.to(self.embed.weight.dtype)) + self.project_inputs(x_inputs).unsqueeze(2) + tok_mask = msa_attention_mask[:, :, 0].bool() + pair_attention_mask = tok_mask.unsqueeze(2) & tok_mask.unsqueeze(1) + for block in self.blocks: + m, x_pair = block(m, x_pair, msa_attention_mask, pair_attention_mask) + return x_pair + + +__all__ = ["ESMFold2Model", "ESMFold2PreTrainedModel"] From 58610a4ce162a71a1e1f384876176fcc7a9d8385 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 12 Jun 2026 19:10:04 +0100 Subject: [PATCH 43/70] More modular cleanup --- src/transformers/models/esmc/modeling_esmc.py | 4 ++++ src/transformers/models/esmc/modular_esmc.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 1ce365fe2343..ba0774ba5053 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -680,6 +680,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -837,6 +838,7 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -948,6 +950,7 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: r""" output_attentions (`bool`, *optional*): @@ -1039,6 +1042,7 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: r""" output_attentions (`bool`, *optional*): diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 2f6ca0f99a55..340219c7e588 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -629,6 +629,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -786,6 +787,7 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -897,6 +899,7 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: r""" output_attentions (`bool`, *optional*): @@ -988,6 +991,7 @@ def forward( output_attentions: bool | None = None, return_dict: bool | None = None, labels: torch.Tensor | None = None, + **kwargs, ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: r""" output_attentions (`bool`, *optional*): From e975f64d71d1c9d8045252cb9fcc843ffd253f4f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Jun 2026 15:42:20 +0100 Subject: [PATCH 44/70] Even more modular cleanup --- docs/source/en/model_doc/esmc.md | 2 +- docs/source/en/model_doc/esmfold2.md | 2 +- src/transformers/conversion_mapping.py | 18 + src/transformers/models/esmc/modeling_esmc.py | 406 ++++++------------ src/transformers/models/esmc/modular_esmc.py | 359 +++------------- 5 files changed, 211 insertions(+), 576 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 05ea1a7a7558..5e3fbe6aa186 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-12.* +*This model was contributed to Hugging Face Transformers on 2026-06-15.* # ESMC diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 469996572264..9d73f99c21ff 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-12.* +*This model was contributed to Hugging Face Transformers on 2026-06-15.* # ESMFold2 diff --git a/src/transformers/conversion_mapping.py b/src/transformers/conversion_mapping.py index 0ac43952d6a7..044e65e18a3f 100755 --- a/src/transformers/conversion_mapping.py +++ b/src/transformers/conversion_mapping.py @@ -790,6 +790,24 @@ def _build_checkpoint_conversion_mapping(): "rotary_embeddings.inv_freq", ), ], + # The published ESMC checkpoints fuse each transformer block's two pre-norms + # into the QKV / FFN projections: ``attn.layernorm_qkv`` is ``LayerNorm + Linear`` + # and ``ffn`` is ``LayerNorm + SwiGLU`` (weights ``fc1_weight`` / ``fc2_weight``). + # The model uses the standard ModernBERT-style pre-norm layout instead -- separate + # block-level ``attn_norm`` / ``mlp_norm`` with fused ``attn.Wqkv`` / ``attn.Wo`` + # and ``mlp.Wi`` / ``mlp.Wo``. Remap on load (and reverse on save). Registered on + # the "esmc" model_type so it also covers the bundled ESMC backbone nested under + # ``esmc.*`` inside an ESMFold2 checkpoint (sub-modules are remapped by model_type). + "esmc": [ + WeightRenaming(r"attn\.layernorm_qkv\.layer_norm_weight", "attn_norm.weight"), + WeightRenaming(r"attn\.layernorm_qkv\.layer_norm_bias", "attn_norm.bias"), + WeightRenaming(r"attn\.layernorm_qkv\.weight", "attn.Wqkv.weight"), + WeightRenaming(r"attn\.out_proj\.", "attn.Wo."), + WeightRenaming(r"ffn\.layer_norm_weight", "mlp_norm.weight"), + WeightRenaming(r"ffn\.layer_norm_bias", "mlp_norm.bias"), + WeightRenaming(r"ffn\.fc1_weight", "mlp.Wi.weight"), + WeightRenaming(r"ffn\.fc2_weight", "mlp.Wo.weight"), + ], # Scoped to the class (not the "esmc" model_type) so it only applies to the # head model: the published ESMC checkpoints store the masked-LM head as an # ``nn.Sequential`` (keys ``lm_head.{0,2,3}``); ``ESMCForMaskedLM`` uses a diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index ba0774ba5053..9365b7fe401a 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -20,7 +20,7 @@ import math from collections.abc import Callable -from dataclasses import dataclass +from typing import Optional import torch import torch.nn as nn @@ -36,171 +36,98 @@ SequenceClassifierOutput, TokenClassifierOutput, ) +from ...modeling_rope_utils import dynamic_rope_update from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel # type: ignore[import] from ...processing_utils import Unpack from ...utils import TransformersKwargs, auto_docstring, can_return_tuple # type: ignore[import] +from ...utils.generic import maybe_autocast from .configuration_esmc import ESMCConfig -# --------------------------------------------------------------------------- -# Output dataclasses -# --------------------------------------------------------------------------- - - -@dataclass -class ESMCMaskedLMOutput(MaskedLMOutput): - """ - Args: - loss (`torch.FloatTensor` of shape `(1,)`, *optional*): - Masked language modelling loss. Returned when ``labels`` are provided. - logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, vocab_size)`): - Prediction scores of the language modelling head. - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Final hidden states after layer normalisation. - hidden_states (`tuple(torch.FloatTensor)`, *optional*): - Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` - (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. - """ - - loss: torch.FloatTensor | None = None - logits: torch.FloatTensor | None = None - last_hidden_state: torch.FloatTensor | None = None - hidden_states: tuple[torch.FloatTensor, ...] | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - -@dataclass -class ESMCTokenClassifierOutput(TokenClassifierOutput): - """ - Args: - loss (`torch.FloatTensor` of shape `(1,)`, *optional*): - Token classification loss. Returned when ``labels`` are provided. - logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, num_labels)`): - Classification scores (before SoftMax). - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Final hidden states after layer normalisation. - hidden_states (`tuple(torch.FloatTensor)`, *optional*): - Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` - (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. - """ - - loss: torch.FloatTensor | None = None - logits: torch.FloatTensor | None = None - last_hidden_state: torch.FloatTensor | None = None - hidden_states: tuple[torch.FloatTensor, ...] | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - -@dataclass -class ESMCSequenceClassifierOutput(SequenceClassifierOutput): - """ - Args: - loss (`torch.FloatTensor` of shape `(1,)`, *optional*): - Sequence classification loss. Returned when ``labels`` are provided. - logits (`torch.FloatTensor` of shape `(batch_size, num_labels)`): - Classification scores (before SoftMax). - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Final hidden states after layer normalisation. - hidden_states (`tuple(torch.FloatTensor)`, *optional*): - Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` - (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. - """ - - loss: torch.FloatTensor | None = None - logits: torch.FloatTensor | None = None - last_hidden_state: torch.FloatTensor | None = None - hidden_states: tuple[torch.FloatTensor, ...] | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - -# --------------------------------------------------------------------------- -# Rotary position embedding helpers -# --------------------------------------------------------------------------- - - class ESMCRotaryEmbedding(nn.Module): - """Rotary position embeddings (RoPE), config-driven, returning ``(cos, sin)``. - - Follows the standard Transformers rotary convention (cf. - ``EsmRotaryEmbedding`` / ``LlamaRotaryEmbedding``): ``inv_freq`` is a - non-persistent fp32 buffer and ``forward`` builds full-head-dim ``cos`` / - ``sin`` in fp32 before casting to the input dtype. + """Rotary position embeddings (RoPE), returning ``(cos, sin)``. + + Identical to :class:`~transformers.models.esm.modeling_esm.EsmRotaryEmbedding` + (``inv_freq = 1 / theta^(arange(0, dim, 2) / dim)`` with ``dim = d_model // n_heads``, + ``cos`` / ``sin`` built in fp32), with two ESMC-specific tweaks: + + * ``inv_freq`` is a **non-persistent** buffer (recomputed from the config, never + stored in the checkpoint). + * ``_apply`` is overridden so ``inv_freq`` stays fp32 even when the module is cast + to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16). + ``nn.Module._apply`` would otherwise round the rotary frequencies to bf16, which + drifts the RoPE angles and is the dominant source of bf16 fork-vs-port divergence + in the ESMFold2 backbone. """ - inv_freq: torch.Tensor + inv_freq: torch.Tensor # fix linting for `register_buffer` def __init__(self, config: ESMCConfig, device=None): super().__init__() + self.config = config - self.register_buffer("inv_freq", self._compute_inv_freq(config, device), persistent=False) + self.rope_type = {} + + curr_inv_freq, curr_attention_scaling = self.compute_default_rope_parameters(self.config, device) + self.register_buffer("inv_freq", curr_inv_freq) + setattr(self, "attention_scaling", curr_attention_scaling) + inv_freq, _ = self.compute_default_rope_parameters(config, device) + self.register_buffer("inv_freq", inv_freq, persistent=False) @staticmethod - def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: - dim = config.d_model // config.n_heads - return 1.0 / ( - config.rope_theta - ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float32) / dim) - ) + def compute_default_rope_parameters( + config: ESMCConfig | None = None, + device: Optional["torch.device"] = None, + seq_len: int | None = None, + ) -> tuple["torch.Tensor", float]: + """ + Computes the inverse frequencies according to the original RoPE implementation + Args: + config ([`~transformers.PreTrainedConfig`]): + The model configuration. + device (`torch.device`): + The device to use for initialization of the inverse frequencies. + seq_len (`int`, *optional*): + The current sequence length. Unused for this type of RoPE. - def _apply(self, fn, recurse=True): - # Keep ``inv_freq`` in fp32 even when the module is cast to bf16/fp16 (e.g. - # when ESMFold2 loads its bundled ESMC backbone in bf16). ``super()._apply`` would - # round the rotary frequencies to bf16, which drifts the RoPE angles and is - # the dominant source of bf16 fork-vs-port divergence in the ESMFold2 backbone. - result = super()._apply(fn, recurse=recurse) - self.register_buffer( - "inv_freq", self._compute_inv_freq(self.config, device=self.inv_freq.device), persistent=False + Returns: + Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the + post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE). + """ + base = config.rope_theta + dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads + + attention_factor = 1.0 # Unused in this type of RoPE + + # Compute the inverse frequencies + inv_freq = 1.0 / ( + base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim) ) - return result + return inv_freq, attention_factor @torch.no_grad() - def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: - inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) + @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) + def forward(self, x, position_ids, layer_type=None): + inv_freq = getattr(self, "inv_freq") + attention_scaling = getattr(self, "attention_scaling") + + inv_freq_expanded = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) position_ids_expanded = position_ids[:, None, :].float() + device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" - with torch.autocast(device_type=device_type, enabled=False): # force fp32 + with maybe_autocast(device_type=device_type, enabled=False): # Force float32 freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) emb = torch.cat((freqs, freqs), dim=-1) - cos = emb.cos() - sin = emb.sin() - return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) - - -class ESMCLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection (the fused QKV projection). + cos = emb.cos() * attention_scaling + sin = emb.sin() * attention_scaling - Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and - ``weight`` to match the published ESMC checkpoint layout. The LayerNorm runs - in the activation dtype (plain ``F.layer_norm``) exactly like the reference: - standalone bf16 → bf16 norm; inside ESMFold2 the backbone is wrapped in a - bf16 autocast (matching the fork's ``_lm_precision_context``), so the norm - then computes in fp32. A no-op in fp32. - """ - - def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: - super().__init__() - self.d_in = d_in - self.eps = eps - self.layer_norm_weight = nn.Parameter(torch.ones(d_in)) - self.layer_norm_bias = nn.Parameter(torch.zeros(d_in)) - self.weight = nn.Parameter(torch.empty(d_out, d_in)) - nn.init.normal_(self.weight, std=0.02) + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) - return F.linear(x, self.weight) + def _apply(self, fn, recurse=True): + result = super()._apply(fn, recurse=recurse) + inv_freq, _ = self.compute_default_rope_parameters(self.config, device=self.inv_freq.device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + return result # --------------------------------------------------------------------------- @@ -213,53 +140,28 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: return int(((expansion_ratio * d_model) + 255) // 256 * 256) -class ESMCSwiGLUMLP(nn.Module): - """LayerNorm + SwiGLU feed-forward network. +class ESMCMLP(nn.Module): + """SwiGLU feed-forward network, reusing :class:`ModernBertMLP`'s gated forward. - Parameters are named ``layer_norm_weight``, ``layer_norm_bias``, - ``fc1_weight`` and ``fc2_weight`` to match the published ESMC checkpoint - layout. ESMC was trained with ``bias=False``, so this network has no biases. + ``ModernBertMLP`` computes ``Wo(act(input) * gate)`` with ``input, gate = + Wi(x).chunk(2, dim=-1)`` — exactly ESMC's SwiGLU once ``act`` is SiLU. ESMC was + trained with ``bias=False`` and no MLP dropout, and rounds the hidden dim to a + multiple of 256. The pre-MLP LayerNorm lives in :class:`ESMCLayer` (``mlp_norm``); + the published checkpoint's ``ffn.fc1_weight`` / ``ffn.fc2_weight`` map onto + ``mlp.Wi`` / ``mlp.Wo`` via ``conversion_mapping.py``. """ - def __init__(self, d_model: int, expansion_ratio: float, eps: float = 1e-5) -> None: + def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: super().__init__() ffn_hidden_size = _swiglu_hidden_dim(expansion_ratio, d_model) - self.hidden_size = d_model - self.ffn_hidden_size = ffn_hidden_size - self.eps = eps - self.layer_norm_weight = nn.Parameter(torch.ones(d_model)) - self.layer_norm_bias = nn.Parameter(torch.zeros(d_model)) - self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, d_model)) - self.fc2_weight = nn.Parameter(torch.empty(d_model, ffn_hidden_size)) - nn.init.normal_(self.fc1_weight, std=0.02) - nn.init.normal_(self.fc2_weight, std=0.02) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm( - x, - (self.hidden_size,), - self.layer_norm_weight, - self.layer_norm_bias, - self.eps, - ) - x = F.linear(x, self.fc1_weight) - x1, x2 = x.chunk(2, dim=-1) - x = F.silu(x1) * x2 - return F.linear(x, self.fc2_weight) - - -class ESMCGeluMLP(nn.Module): - """LayerNorm + GELU feed-forward network (the non-default ``ffn_type='gelu'``).""" - - def __init__(self, d_model: int, expansion_ratio: float, bias: bool = False) -> None: - super().__init__() - hidden = int(expansion_ratio * d_model) - self.layer_norm = nn.LayerNorm(d_model) - self.fc1 = nn.Linear(d_model, hidden, bias=bias) - self.fc2 = nn.Linear(hidden, d_model, bias=bias) + self.Wi = nn.Linear(d_model, 2 * ffn_hidden_size, bias=False) + self.act = nn.SiLU() + self.drop = nn.Identity() + self.Wo = nn.Linear(ffn_hidden_size, d_model, bias=False) - def forward(self, x: torch.Tensor) -> torch.Tensor: - return self.fc2(F.gelu(self.fc1(self.layer_norm(x)))) + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + input, gate = self.Wi(hidden_states).chunk(2, dim=-1) + return self.Wo(self.drop(self.act(input) * gate)) def rotate_half(x): @@ -361,8 +263,13 @@ def __init__( # attention_mask is passed (unpadded inputs), silently masking causally. self.is_causal = False - self.layernorm_qkv = ESMCLayerNormLinear(d_model, d_model * 3) - self.out_proj = nn.Linear(d_model, d_model, bias=bias) + # Fused QKV projection (``Wqkv``) and output projection (``Wo``), matching + # ModernBERT's attention layout. The pre-attention LayerNorm lives in + # :class:`ESMCLayer` (``attn_norm``); the published checkpoint's fused + # ``attn.layernorm_qkv`` / ``attn.out_proj`` map onto these via + # ``conversion_mapping.py``. + self.Wqkv = nn.Linear(d_model, d_model * 3, bias=bias) + self.Wo = nn.Linear(d_model, d_model, bias=bias) if qk_layernorm: self.q_ln = nn.LayerNorm(d_model, bias=bias) @@ -390,7 +297,7 @@ def forward( ``eager`` interface so they are observable. """ b, s, _ = hidden_states.shape - qkv = self.layernorm_qkv(hidden_states) + qkv = self.Wqkv(hidden_states) q, k, v = torch.chunk(qkv, 3, dim=-1) q = self.q_ln(q).to(q.dtype) k = self.k_ln(k).to(q.dtype) @@ -421,7 +328,7 @@ def forward( **kwargs, ) attn_output = attn_output.reshape(b, s, -1) - return self.out_proj(attn_output), attn_weights + return self.Wo(attn_output), attn_weights # --------------------------------------------------------------------------- @@ -441,7 +348,6 @@ class ESMCLayer(nn.Module): residue_scaling_factor: Scales residual connections to stabilise deep networks (``1 / sqrt(n_layers / 36)`` is the ESM3 scheme). qk_layernorm: Whether to apply QK LayerNorm in attention. - ffn_type: Feed-forward activation: ``"swiglu"`` or ``"gelu"``. """ def __init__( @@ -450,23 +356,21 @@ def __init__( d_model: int, n_heads: int, bias: bool = False, - expansion_ratio: float = 4.0, + expansion_ratio: float = 8 / 3, residue_scaling_factor: float = 1.0, qk_layernorm: bool = True, - ffn_type: str = "swiglu", ): super().__init__() if bias: raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") + # Pre-norm layout (cf. ModernBERT): the LayerNorms that the published ESMC + # checkpoint fuses into ``attn.layernorm_qkv`` / ``ffn`` live here as + # ``attn_norm`` / ``mlp_norm`` (remapped in ``conversion_mapping.py``). + self.attn_norm = nn.LayerNorm(d_model) self.attn = ESMCAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) - - if ffn_type == "swiglu": - self.ffn = ESMCSwiGLUMLP(d_model, expansion_ratio) - elif ffn_type == "gelu": - self.ffn = ESMCGeluMLP(d_model, expansion_ratio, bias) - else: - raise ValueError(f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'.") + self.mlp_norm = nn.LayerNorm(d_model) + self.mlp = ESMCMLP(d_model, expansion_ratio) self.scaling_factor = residue_scaling_factor @@ -493,14 +397,14 @@ def forward( ``(batch, num_heads, seq_len, seq_len)`` or ``None``. """ attn_out, attn_weights = self.attn( - x, + self.attn_norm(x), attention_mask, position_embeddings=position_embeddings, output_attentions=output_attentions, **kwargs, ) x = x + attn_out / self.scaling_factor - x = x + self.ffn(x) / self.scaling_factor + x = x + self.mlp(self.mlp_norm(x)) / self.scaling_factor return x, attn_weights @@ -515,7 +419,6 @@ class ESMCEncoder(nn.Module): ``sqrt(n_layers / 36)`` to each block. bias: Bias flag forwarded to every sub-module. qk_layernorm: QK LayerNorm flag forwarded to every block. - ffn_type: FFN activation type (``"swiglu"`` or ``"gelu"``). expansion_ratio: FFN expansion ratio. use_flash_attn: Use Flash Attention 2 kernel when available. """ @@ -529,7 +432,6 @@ def __init__( scale_residue: bool = True, bias: bool = False, qk_layernorm: bool = True, - ffn_type: str = "swiglu", expansion_ratio: float = 8 / 3, ): super().__init__() @@ -543,7 +445,6 @@ def __init__( expansion_ratio=expansion_ratio, bias=bias, qk_layernorm=qk_layernorm, - ffn_type=ffn_type, ) for _ in range(n_layers) ] @@ -625,21 +526,9 @@ class ESMCPreTrainedModel(PreTrainedModel): _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] def _init_weights(self, module: nn.Module): - std = self.config.initializer_range - # The fused LN+projection modules are handled explicitly: the base - # `_init_weights` matches norms by class-name substring ("LayerNorm"), - # which would otherwise mis-initialize their (Linear) `weight` to ones. - if isinstance(module, ESMCLayerNormLinear): - init.ones_(module.layer_norm_weight) - init.zeros_(module.layer_norm_bias) - init.normal_(module.weight, mean=0.0, std=std) - elif isinstance(module, ESMCSwiGLUMLP): - init.ones_(module.layer_norm_weight) - init.zeros_(module.layer_norm_bias) - init.normal_(module.fc1_weight, mean=0.0, std=std) - init.normal_(module.fc2_weight, mean=0.0, std=std) - elif isinstance(module, ESMCRotaryEmbedding): - init.copy_(module.inv_freq, module._compute_inv_freq(module.config)) + if isinstance(module, ESMCRotaryEmbedding): + inv_freq, _ = module.compute_default_rope_parameters(module.config) + init.copy_(module.inv_freq, inv_freq) else: # nn.Linear / nn.Embedding / nn.LayerNorm via the base initializer. super()._init_weights(module) @@ -836,10 +725,9 @@ def forward( sequence_id: torch.Tensor | None = None, output_hidden_states: bool | None = None, output_attentions: bool | None = None, - return_dict: bool | None = None, labels: torch.Tensor | None = None, **kwargs, - ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: + ) -> tuple[torch.Tensor, ...] | MaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor forwarded to the encoder for chain-aware @@ -865,8 +753,6 @@ def forward( torch.Size([1, 11, 64]) ``` """ - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - encoder_outputs = self.esmc( input_ids=input_ids, attention_mask=attention_mask, @@ -882,35 +768,21 @@ def forward( if labels is not None: loss = CrossEntropyLoss(ignore_index=-100)(logits.view(-1, self.config.vocab_size), labels.view(-1)) - if not return_dict: - return tuple( - v - for v in [ - loss, - logits, - encoder_outputs.last_hidden_state, - encoder_outputs.hidden_states, - encoder_outputs.attentions, - ] - if v is not None - ) - - return ESMCMaskedLMOutput( + return MaskedLMOutput( loss=loss, logits=logits, - last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, attentions=encoder_outputs.attentions, ) -# --------------------------------------------------------------------------- -# Classification heads -# --------------------------------------------------------------------------- - - class ESMCClassificationHead(nn.Module): - """Dense classification head applied to the ```` token representation.""" + """Dense classification head applied to the ```` token representation. + + Identical to :class:`~transformers.models.esm.modeling_esm.EsmClassificationHead` + (```` token -> dropout -> ``tanh(dense)`` -> dropout -> ``out_proj``); ESMC just + sources the dropout rate from ``classifier_dropout`` instead of ``hidden_dropout_prob``. + """ def __init__(self, config: ESMCConfig): super().__init__() @@ -918,12 +790,14 @@ def __init__(self, config: ESMCConfig): self.dropout = nn.Dropout(config.classifier_dropout) self.out_proj = nn.Linear(config.d_model, config.num_labels) - def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: - x = hidden_states[:, 0, :] # token + def forward(self, features, **kwargs): + x = features[:, 0, :] # take token (equiv. to [CLS]) x = self.dropout(x) - x = torch.tanh(self.dense(x)) + x = self.dense(x) + x = torch.tanh(x) x = self.dropout(x) - return self.out_proj(x) + x = self.out_proj(x) + return x # --------------------------------------------------------------------------- @@ -948,10 +822,9 @@ def forward( attention_mask: torch.Tensor | None = None, output_hidden_states: bool | None = None, output_attentions: bool | None = None, - return_dict: bool | None = None, labels: torch.Tensor | None = None, **kwargs, - ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: + ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: r""" output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the @@ -961,8 +834,6 @@ def forward( ``[0, config.num_labels - 1]``. For regression pass a float tensor of shape ``(batch_size,)``. """ - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - encoder_outputs = self.esmc( input_ids, attention_mask=attention_mask, @@ -995,23 +866,9 @@ def forward( elif self.config.problem_type == "multi_label_classification": loss = BCEWithLogitsLoss()(logits, labels) - if not return_dict: - return tuple( - v - for v in [ - loss, - logits, - encoder_outputs.last_hidden_state, - encoder_outputs.hidden_states, - encoder_outputs.attentions, - ] - if v is not None - ) - - return ESMCSequenceClassifierOutput( + return SequenceClassifierOutput( loss=loss, logits=logits, - last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, attentions=encoder_outputs.attentions, ) @@ -1040,10 +897,9 @@ def forward( attention_mask: torch.Tensor | None = None, output_hidden_states: bool | None = None, output_attentions: bool | None = None, - return_dict: bool | None = None, labels: torch.Tensor | None = None, **kwargs, - ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: + ) -> tuple[torch.Tensor, ...] | TokenClassifierOutput: r""" output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the @@ -1052,8 +908,6 @@ def forward( Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. Positions with index ``-100`` are ignored in the loss. """ - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - encoder_outputs = self.esmc( input_ids=input_ids, attention_mask=attention_mask, @@ -1071,23 +925,9 @@ def forward( logits.view(-1, self.num_labels), labels.to(logits.device).view(-1) ) - if not return_dict: - return tuple( - v - for v in [ - loss, - logits, - encoder_outputs.last_hidden_state, - encoder_outputs.hidden_states, - encoder_outputs.attentions, - ] - if v is not None - ) - - return ESMCTokenClassifierOutput( + return TokenClassifierOutput( loss=loss, logits=logits, - last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, attentions=encoder_outputs.attentions, ) diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 340219c7e588..5d4a010fa89e 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -15,7 +15,6 @@ import math from collections.abc import Callable -from dataclasses import dataclass import torch import torch.nn as nn @@ -36,7 +35,7 @@ can_return_tuple, logging, ) -from ..esm.modeling_esm import eager_attention_forward +from ..esm.modeling_esm import EsmClassificationHead, EsmRotaryEmbedding, eager_attention_forward # ESMC applies RoPE in the activation dtype (no fp32 upcast), matching the reference # implementation's bf16 numerics. Llama's `apply_rotary_pos_emb` is exactly that @@ -45,6 +44,7 @@ # ESMC-6B), so we reuse Llama's rather than esm's. `rotate_half` is pulled in by the # modular converter as a dependency of the imported function. from ..llama.modeling_llama import apply_rotary_pos_emb +from ..modernbert.modeling_modernbert import ModernBertMLP from .configuration_esmc import ESMCConfig @@ -52,141 +52,38 @@ _CONFIG_FOR_DOC = "ESMCConfig" -# --------------------------------------------------------------------------- -# Output dataclasses -# --------------------------------------------------------------------------- - - -@dataclass -class ESMCMaskedLMOutput(MaskedLMOutput): - """ - Args: - loss (`torch.FloatTensor` of shape `(1,)`, *optional*): - Masked language modelling loss. Returned when ``labels`` are provided. - logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, vocab_size)`): - Prediction scores of the language modelling head. - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Final hidden states after layer normalisation. - hidden_states (`tuple(torch.FloatTensor)`, *optional*): - Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` - (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. - """ - - loss: torch.FloatTensor | None = None - logits: torch.FloatTensor | None = None - last_hidden_state: torch.FloatTensor | None = None - hidden_states: tuple[torch.FloatTensor, ...] | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - -@dataclass -class ESMCTokenClassifierOutput(TokenClassifierOutput): - """ - Args: - loss (`torch.FloatTensor` of shape `(1,)`, *optional*): - Token classification loss. Returned when ``labels`` are provided. - logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, num_labels)`): - Classification scores (before SoftMax). - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Final hidden states after layer normalisation. - hidden_states (`tuple(torch.FloatTensor)`, *optional*): - Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` - (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. - """ - - loss: torch.FloatTensor | None = None - logits: torch.FloatTensor | None = None - last_hidden_state: torch.FloatTensor | None = None - hidden_states: tuple[torch.FloatTensor, ...] | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - -@dataclass -class ESMCSequenceClassifierOutput(SequenceClassifierOutput): - """ - Args: - loss (`torch.FloatTensor` of shape `(1,)`, *optional*): - Sequence classification loss. Returned when ``labels`` are provided. - logits (`torch.FloatTensor` of shape `(batch_size, num_labels)`): - Classification scores (before SoftMax). - last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, d_model)`): - Final hidden states after layer normalisation. - hidden_states (`tuple(torch.FloatTensor)`, *optional*): - Tuple of per-layer hidden states, each of shape ``(batch_size, sequence_length, d_model)`` - (the embedding output plus one per encoder layer). Returned when ``output_hidden_states=True``. - attentions (`tuple(torch.FloatTensor)`, *optional*): - Per-layer attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Returned when ``output_attentions=True``. - """ - - loss: torch.FloatTensor | None = None - logits: torch.FloatTensor | None = None - last_hidden_state: torch.FloatTensor | None = None - hidden_states: tuple[torch.FloatTensor, ...] | None = None - attentions: tuple[torch.FloatTensor, ...] | None = None - - # --------------------------------------------------------------------------- # Rotary position embedding helpers # --------------------------------------------------------------------------- -class ESMCRotaryEmbedding(nn.Module): - """Rotary position embeddings (RoPE), config-driven, returning ``(cos, sin)``. +class ESMCRotaryEmbedding(EsmRotaryEmbedding): + """Rotary position embeddings (RoPE), returning ``(cos, sin)``. - Follows the standard Transformers rotary convention (cf. - ``EsmRotaryEmbedding`` / ``LlamaRotaryEmbedding``): ``inv_freq`` is a - non-persistent fp32 buffer and ``forward`` builds full-head-dim ``cos`` / - ``sin`` in fp32 before casting to the input dtype. - """ + Identical to :class:`~transformers.models.esm.modeling_esm.EsmRotaryEmbedding` + (``inv_freq = 1 / theta^(arange(0, dim, 2) / dim)`` with ``dim = d_model // n_heads``, + ``cos`` / ``sin`` built in fp32), with two ESMC-specific tweaks: - inv_freq: torch.Tensor + * ``inv_freq`` is a **non-persistent** buffer (recomputed from the config, never + stored in the checkpoint). + * ``_apply`` is overridden so ``inv_freq`` stays fp32 even when the module is cast + to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16). + ``nn.Module._apply`` would otherwise round the rotary frequencies to bf16, which + drifts the RoPE angles and is the dominant source of bf16 fork-vs-port divergence + in the ESMFold2 backbone. + """ def __init__(self, config: ESMCConfig, device=None): - super().__init__() - self.config = config - self.register_buffer("inv_freq", self._compute_inv_freq(config, device), persistent=False) - - @staticmethod - def _compute_inv_freq(config: ESMCConfig, device=None) -> torch.Tensor: - dim = config.d_model // config.n_heads - return 1.0 / ( - config.rope_theta - ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float32) / dim) - ) + super().__init__(config, device=device) + inv_freq, _ = self.compute_default_rope_parameters(config, device) + self.register_buffer("inv_freq", inv_freq, persistent=False) def _apply(self, fn, recurse=True): - # Keep ``inv_freq`` in fp32 even when the module is cast to bf16/fp16 (e.g. - # when ESMFold2 loads its bundled ESMC backbone in bf16). ``super()._apply`` would - # round the rotary frequencies to bf16, which drifts the RoPE angles and is - # the dominant source of bf16 fork-vs-port divergence in the ESMFold2 backbone. result = super()._apply(fn, recurse=recurse) - self.register_buffer( - "inv_freq", self._compute_inv_freq(self.config, device=self.inv_freq.device), persistent=False - ) + inv_freq, _ = self.compute_default_rope_parameters(self.config, device=self.inv_freq.device) + self.register_buffer("inv_freq", inv_freq, persistent=False) return result - @torch.no_grad() - def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: - inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) - position_ids_expanded = position_ids[:, None, :].float() - device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" - with torch.autocast(device_type=device_type, enabled=False): # force fp32 - freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) - emb = torch.cat((freqs, freqs), dim=-1) - cos = emb.cos() - sin = emb.sin() - return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) - # --------------------------------------------------------------------------- # Feed-forward network helpers @@ -198,78 +95,24 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: return int(((expansion_ratio * d_model) + 255) // 256 * 256) -class ESMCLayerNormLinear(nn.Module): - """LayerNorm followed by a Linear projection (the fused QKV projection). - - Parameters are named ``layer_norm_weight``, ``layer_norm_bias`` and - ``weight`` to match the published ESMC checkpoint layout. The LayerNorm runs - in the activation dtype (plain ``F.layer_norm``) exactly like the reference: - standalone bf16 → bf16 norm; inside ESMFold2 the backbone is wrapped in a - bf16 autocast (matching the fork's ``_lm_precision_context``), so the norm - then computes in fp32. A no-op in fp32. - """ - - def __init__(self, d_in: int, d_out: int, eps: float = 1e-5) -> None: - super().__init__() - self.d_in = d_in - self.eps = eps - self.layer_norm_weight = nn.Parameter(torch.ones(d_in)) - self.layer_norm_bias = nn.Parameter(torch.zeros(d_in)) - self.weight = nn.Parameter(torch.empty(d_out, d_in)) - nn.init.normal_(self.weight, std=0.02) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm(x, (self.d_in,), self.layer_norm_weight, self.layer_norm_bias, self.eps) - return F.linear(x, self.weight) - - -class ESMCSwiGLUMLP(nn.Module): - """LayerNorm + SwiGLU feed-forward network. +class ESMCMLP(ModernBertMLP): + """SwiGLU feed-forward network, reusing :class:`ModernBertMLP`'s gated forward. - Parameters are named ``layer_norm_weight``, ``layer_norm_bias``, - ``fc1_weight`` and ``fc2_weight`` to match the published ESMC checkpoint - layout. ESMC was trained with ``bias=False``, so this network has no biases. + ``ModernBertMLP`` computes ``Wo(act(input) * gate)`` with ``input, gate = + Wi(x).chunk(2, dim=-1)`` — exactly ESMC's SwiGLU once ``act`` is SiLU. ESMC was + trained with ``bias=False`` and no MLP dropout, and rounds the hidden dim to a + multiple of 256. The pre-MLP LayerNorm lives in :class:`ESMCLayer` (``mlp_norm``); + the published checkpoint's ``ffn.fc1_weight`` / ``ffn.fc2_weight`` map onto + ``mlp.Wi`` / ``mlp.Wo`` via ``conversion_mapping.py``. """ - def __init__(self, d_model: int, expansion_ratio: float, eps: float = 1e-5) -> None: - super().__init__() + def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: + nn.Module.__init__(self) ffn_hidden_size = _swiglu_hidden_dim(expansion_ratio, d_model) - self.hidden_size = d_model - self.ffn_hidden_size = ffn_hidden_size - self.eps = eps - self.layer_norm_weight = nn.Parameter(torch.ones(d_model)) - self.layer_norm_bias = nn.Parameter(torch.zeros(d_model)) - self.fc1_weight = nn.Parameter(torch.empty(2 * ffn_hidden_size, d_model)) - self.fc2_weight = nn.Parameter(torch.empty(d_model, ffn_hidden_size)) - nn.init.normal_(self.fc1_weight, std=0.02) - nn.init.normal_(self.fc2_weight, std=0.02) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = F.layer_norm( - x, - (self.hidden_size,), - self.layer_norm_weight, - self.layer_norm_bias, - self.eps, - ) - x = F.linear(x, self.fc1_weight) - x1, x2 = x.chunk(2, dim=-1) - x = F.silu(x1) * x2 - return F.linear(x, self.fc2_weight) - - -class ESMCGeluMLP(nn.Module): - """LayerNorm + GELU feed-forward network (the non-default ``ffn_type='gelu'``).""" - - def __init__(self, d_model: int, expansion_ratio: float, bias: bool = False) -> None: - super().__init__() - hidden = int(expansion_ratio * d_model) - self.layer_norm = nn.LayerNorm(d_model) - self.fc1 = nn.Linear(d_model, hidden, bias=bias) - self.fc2 = nn.Linear(hidden, d_model, bias=bias) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - return self.fc2(F.gelu(self.fc1(self.layer_norm(x)))) + self.Wi = nn.Linear(d_model, 2 * ffn_hidden_size, bias=False) + self.act = nn.SiLU() + self.drop = nn.Identity() + self.Wo = nn.Linear(ffn_hidden_size, d_model, bias=False) # --------------------------------------------------------------------------- @@ -310,8 +153,13 @@ def __init__( # attention_mask is passed (unpadded inputs), silently masking causally. self.is_causal = False - self.layernorm_qkv = ESMCLayerNormLinear(d_model, d_model * 3) - self.out_proj = nn.Linear(d_model, d_model, bias=bias) + # Fused QKV projection (``Wqkv``) and output projection (``Wo``), matching + # ModernBERT's attention layout. The pre-attention LayerNorm lives in + # :class:`ESMCLayer` (``attn_norm``); the published checkpoint's fused + # ``attn.layernorm_qkv`` / ``attn.out_proj`` map onto these via + # ``conversion_mapping.py``. + self.Wqkv = nn.Linear(d_model, d_model * 3, bias=bias) + self.Wo = nn.Linear(d_model, d_model, bias=bias) if qk_layernorm: self.q_ln = nn.LayerNorm(d_model, bias=bias) @@ -339,7 +187,7 @@ def forward( ``eager`` interface so they are observable. """ b, s, _ = hidden_states.shape - qkv = self.layernorm_qkv(hidden_states) + qkv = self.Wqkv(hidden_states) q, k, v = torch.chunk(qkv, 3, dim=-1) q = self.q_ln(q).to(q.dtype) k = self.k_ln(k).to(q.dtype) @@ -370,7 +218,7 @@ def forward( **kwargs, ) attn_output = attn_output.reshape(b, s, -1) - return self.out_proj(attn_output), attn_weights + return self.Wo(attn_output), attn_weights # --------------------------------------------------------------------------- @@ -390,7 +238,6 @@ class ESMCLayer(nn.Module): residue_scaling_factor: Scales residual connections to stabilise deep networks (``1 / sqrt(n_layers / 36)`` is the ESM3 scheme). qk_layernorm: Whether to apply QK LayerNorm in attention. - ffn_type: Feed-forward activation: ``"swiglu"`` or ``"gelu"``. """ def __init__( @@ -399,23 +246,21 @@ def __init__( d_model: int, n_heads: int, bias: bool = False, - expansion_ratio: float = 4.0, + expansion_ratio: float = 8 / 3, residue_scaling_factor: float = 1.0, qk_layernorm: bool = True, - ffn_type: str = "swiglu", ): super().__init__() if bias: raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") + # Pre-norm layout (cf. ModernBERT): the LayerNorms that the published ESMC + # checkpoint fuses into ``attn.layernorm_qkv`` / ``ffn`` live here as + # ``attn_norm`` / ``mlp_norm`` (remapped in ``conversion_mapping.py``). + self.attn_norm = nn.LayerNorm(d_model) self.attn = ESMCAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) - - if ffn_type == "swiglu": - self.ffn = ESMCSwiGLUMLP(d_model, expansion_ratio) - elif ffn_type == "gelu": - self.ffn = ESMCGeluMLP(d_model, expansion_ratio, bias) - else: - raise ValueError(f"Unknown ffn_type: {ffn_type!r}. Choose 'swiglu' or 'gelu'.") + self.mlp_norm = nn.LayerNorm(d_model) + self.mlp = ESMCMLP(d_model, expansion_ratio) self.scaling_factor = residue_scaling_factor @@ -442,14 +287,14 @@ def forward( ``(batch, num_heads, seq_len, seq_len)`` or ``None``. """ attn_out, attn_weights = self.attn( - x, + self.attn_norm(x), attention_mask, position_embeddings=position_embeddings, output_attentions=output_attentions, **kwargs, ) x = x + attn_out / self.scaling_factor - x = x + self.ffn(x) / self.scaling_factor + x = x + self.mlp(self.mlp_norm(x)) / self.scaling_factor return x, attn_weights @@ -464,7 +309,6 @@ class ESMCEncoder(nn.Module): ``sqrt(n_layers / 36)`` to each block. bias: Bias flag forwarded to every sub-module. qk_layernorm: QK LayerNorm flag forwarded to every block. - ffn_type: FFN activation type (``"swiglu"`` or ``"gelu"``). expansion_ratio: FFN expansion ratio. use_flash_attn: Use Flash Attention 2 kernel when available. """ @@ -478,7 +322,6 @@ def __init__( scale_residue: bool = True, bias: bool = False, qk_layernorm: bool = True, - ffn_type: str = "swiglu", expansion_ratio: float = 8 / 3, ): super().__init__() @@ -492,7 +335,6 @@ def __init__( expansion_ratio=expansion_ratio, bias=bias, qk_layernorm=qk_layernorm, - ffn_type=ffn_type, ) for _ in range(n_layers) ] @@ -574,21 +416,9 @@ class ESMCPreTrainedModel(PreTrainedModel): _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] def _init_weights(self, module: nn.Module): - std = self.config.initializer_range - # The fused LN+projection modules are handled explicitly: the base - # `_init_weights` matches norms by class-name substring ("LayerNorm"), - # which would otherwise mis-initialize their (Linear) `weight` to ones. - if isinstance(module, ESMCLayerNormLinear): - init.ones_(module.layer_norm_weight) - init.zeros_(module.layer_norm_bias) - init.normal_(module.weight, mean=0.0, std=std) - elif isinstance(module, ESMCSwiGLUMLP): - init.ones_(module.layer_norm_weight) - init.zeros_(module.layer_norm_bias) - init.normal_(module.fc1_weight, mean=0.0, std=std) - init.normal_(module.fc2_weight, mean=0.0, std=std) - elif isinstance(module, ESMCRotaryEmbedding): - init.copy_(module.inv_freq, module._compute_inv_freq(module.config)) + if isinstance(module, ESMCRotaryEmbedding): + inv_freq, _ = module.compute_default_rope_parameters(module.config) + init.copy_(module.inv_freq, inv_freq) else: # nn.Linear / nn.Embedding / nn.LayerNorm via the base initializer. super()._init_weights(module) @@ -785,10 +615,9 @@ def forward( sequence_id: torch.Tensor | None = None, output_hidden_states: bool | None = None, output_attentions: bool | None = None, - return_dict: bool | None = None, labels: torch.Tensor | None = None, **kwargs, - ) -> tuple[torch.Tensor, ...] | ESMCMaskedLMOutput: + ) -> tuple[torch.Tensor, ...] | MaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor forwarded to the encoder for chain-aware @@ -814,8 +643,6 @@ def forward( torch.Size([1, 11, 64]) ``` """ - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - encoder_outputs = self.esmc( input_ids=input_ids, attention_mask=attention_mask, @@ -831,23 +658,9 @@ def forward( if labels is not None: loss = CrossEntropyLoss(ignore_index=-100)(logits.view(-1, self.config.vocab_size), labels.view(-1)) - if not return_dict: - return tuple( - v - for v in [ - loss, - logits, - encoder_outputs.last_hidden_state, - encoder_outputs.hidden_states, - encoder_outputs.attentions, - ] - if v is not None - ) - - return ESMCMaskedLMOutput( + return MaskedLMOutput( loss=loss, logits=logits, - last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, attentions=encoder_outputs.attentions, ) @@ -858,8 +671,13 @@ def forward( # --------------------------------------------------------------------------- -class ESMCClassificationHead(nn.Module): - """Dense classification head applied to the ```` token representation.""" +class ESMCClassificationHead(EsmClassificationHead): + """Dense classification head applied to the ```` token representation. + + Identical to :class:`~transformers.models.esm.modeling_esm.EsmClassificationHead` + (```` token -> dropout -> ``tanh(dense)`` -> dropout -> ``out_proj``); ESMC just + sources the dropout rate from ``classifier_dropout`` instead of ``hidden_dropout_prob``. + """ def __init__(self, config: ESMCConfig): super().__init__() @@ -867,13 +685,6 @@ def __init__(self, config: ESMCConfig): self.dropout = nn.Dropout(config.classifier_dropout) self.out_proj = nn.Linear(config.d_model, config.num_labels) - def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: - x = hidden_states[:, 0, :] # token - x = self.dropout(x) - x = torch.tanh(self.dense(x)) - x = self.dropout(x) - return self.out_proj(x) - # --------------------------------------------------------------------------- # Sequence classification @@ -897,10 +708,9 @@ def forward( attention_mask: torch.Tensor | None = None, output_hidden_states: bool | None = None, output_attentions: bool | None = None, - return_dict: bool | None = None, labels: torch.Tensor | None = None, **kwargs, - ) -> tuple[torch.Tensor, ...] | ESMCSequenceClassifierOutput: + ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: r""" output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the @@ -910,8 +720,6 @@ def forward( ``[0, config.num_labels - 1]``. For regression pass a float tensor of shape ``(batch_size,)``. """ - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - encoder_outputs = self.esmc( input_ids, attention_mask=attention_mask, @@ -944,23 +752,9 @@ def forward( elif self.config.problem_type == "multi_label_classification": loss = BCEWithLogitsLoss()(logits, labels) - if not return_dict: - return tuple( - v - for v in [ - loss, - logits, - encoder_outputs.last_hidden_state, - encoder_outputs.hidden_states, - encoder_outputs.attentions, - ] - if v is not None - ) - - return ESMCSequenceClassifierOutput( + return SequenceClassifierOutput( loss=loss, logits=logits, - last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, attentions=encoder_outputs.attentions, ) @@ -989,10 +783,9 @@ def forward( attention_mask: torch.Tensor | None = None, output_hidden_states: bool | None = None, output_attentions: bool | None = None, - return_dict: bool | None = None, labels: torch.Tensor | None = None, **kwargs, - ) -> tuple[torch.Tensor, ...] | ESMCTokenClassifierOutput: + ) -> tuple[torch.Tensor, ...] | TokenClassifierOutput: r""" output_attentions (`bool`, *optional*): Whether to return per-block attention weights. Forwarded to the @@ -1001,8 +794,6 @@ def forward( Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. Positions with index ``-100`` are ignored in the loss. """ - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - encoder_outputs = self.esmc( input_ids=input_ids, attention_mask=attention_mask, @@ -1020,23 +811,9 @@ def forward( logits.view(-1, self.num_labels), labels.to(logits.device).view(-1) ) - if not return_dict: - return tuple( - v - for v in [ - loss, - logits, - encoder_outputs.last_hidden_state, - encoder_outputs.hidden_states, - encoder_outputs.attentions, - ] - if v is not None - ) - - return ESMCTokenClassifierOutput( + return TokenClassifierOutput( loss=loss, logits=logits, - last_hidden_state=encoder_outputs.last_hidden_state, hidden_states=encoder_outputs.hidden_states, attentions=encoder_outputs.attentions, ) From f0c8b771332477d508f708bc6584a5ad0980816b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Jun 2026 15:58:51 +0100 Subject: [PATCH 45/70] No more modular ESMFold2 --- .../models/esmfold2/modeling_esmfold2.py | 9 +- .../models/esmfold2/modular_esmfold2.py | 3174 ----------------- 2 files changed, 3 insertions(+), 3180 deletions(-) delete mode 100644 src/transformers/models/esmfold2/modular_esmfold2.py diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index c567b373ee8a..4899d1048efc 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -1,9 +1,3 @@ -# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 -# This file was automatically generated from src/transformers/models/esmfold2/modular_esmfold2.py. -# Do NOT edit this file manually as any edits will be overwritten by the generation of -# the file from the modular. If any change should be done, please apply the change to the -# modular_esmfold2.py file directly. One of our CI enforces this. -# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 # Copyright 2026 Biohub. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -196,6 +190,7 @@ def forward(self, x: Tensor) -> Tensor: return self.w_down(F.silu(x1) * x2) +# Copied from transformers.models.llama.modeling_llama.rotate_half def rotate_half(x): """Rotates half the hidden dims of the input.""" x1 = x[..., : x.shape[-1] // 2] @@ -203,6 +198,7 @@ def rotate_half(x): return torch.cat((-x2, x1), dim=-1) +# Copied from transformers.models.llama.modeling_llama.repeat_kv def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: """ This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, @@ -215,6 +211,7 @@ def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) +# Copied from transformers.models.llama.modeling_llama.eager_attention_forward def eager_attention_forward( module: nn.Module, query: torch.Tensor, diff --git a/src/transformers/models/esmfold2/modular_esmfold2.py b/src/transformers/models/esmfold2/modular_esmfold2.py deleted file mode 100644 index bfbd1ec1989e..000000000000 --- a/src/transformers/models/esmfold2/modular_esmfold2.py +++ /dev/null @@ -1,3174 +0,0 @@ -# Copyright 2026 Biohub. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""PyTorch ESMFold2 model — the standard released architecture. - -Quickstart:: - - from transformers import ESMFold2Model - - model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).cuda().eval() - open("ubq.pdb", "w").write(model.infer_protein_as_pdb("MQIFVKTLTGKT...")) - -``infer_protein`` / ``infer_protein_as_pdb`` cover single-chain protein folding from a -sequence; multi-chain, ligand, or MSA inputs are supported by building the structural -feature tensors yourself and passing them directly to ``forward``. -""" - -from __future__ import annotations - -import math -from collections.abc import Callable -from dataclasses import dataclass -from functools import partial - -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch import Tensor -from torch.utils.checkpoint import checkpoint - -from ... import initialization as init -from ...integrations import use_kernel_forward_from_hub -from ...modeling_outputs import ModelOutput -from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel -from ...utils import is_flash_attn_2_available -from ..auto import AutoModel - -# ESMFold2 reuses Llama's plain attention helpers verbatim. `eager_attention_forward` -# upcasts the softmax to fp32 (Llama's variant, NOT esm's no-upcast one) which the -# bit-exact bf16 backbone relies on, and pulls in `repeat_kv` as a dependency. The -# atom-track 3D RoPE (`apply_rotary_emb_3d`) reuses Llama's `rotate_half`. -from ..llama.modeling_llama import eager_attention_forward, rotate_half -from .configuration_esmfold2 import ESMFold2Config - - -if is_flash_attn_2_available(): - from flash_attn import flash_attn_func, flash_attn_varlen_func - from flash_attn.bert_padding import index_first_axis, pad_input - - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- -CHAR_VOCAB_SIZE = 64 -MAX_CHARS = 4 -XYZ_DIMS = 3 -MAX_ATOMIC_NUMBER = 128 - -# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 -ATOM_FEATURE_DIM = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS - - -NUM_RES_TYPES = 33 - -_EPS = 1e-5 - -# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory -# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; -# chunk=64 leaves headroom for the largest foldbench targets). Override via -# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for -# short L but OOM-prone past ~600). -_DEFAULT_CHUNK_SIZE = 64 - - -# =========================================================================== -# Atom-token utilities -# =========================================================================== - - -def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: - """Broadcast per-token features to per-atom features using gather. - - Args: - token_features: [B, L, d] - atom_to_token_idx: [B, A] int64 - - Returns: - [B, A, d] - """ - idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) - return torch.gather(token_features, 1, idx) - - -def scatter_atom_to_token( - atom_features: Tensor, - atom_to_token_idx: Tensor, - n_tokens: int, - atom_mask: Tensor | None = None, -) -> Tensor: - """Aggregate per-atom features to per-token features (mean). - - Args: - atom_features: [B, A, d] - atom_to_token_idx: [B, A] int64 - n_tokens: L - atom_mask: [B, A] bool - - Returns: - [B, L, d] - """ - B, A, d = atom_features.shape - n_out = n_tokens - idx = atom_to_token_idx - if atom_mask is not None: - idx = torch.where(atom_mask, atom_to_token_idx, n_tokens) - n_out = n_tokens + 1 - idx_expanded = idx.unsqueeze(-1).expand(B, A, d) - out = torch.zeros(B, n_out, d, device=atom_features.device, dtype=atom_features.dtype) - out.scatter_reduce_(1, idx_expanded, atom_features, reduce="mean", include_self=False) - return out[:, :n_tokens, :] - - -def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: - """Gather representative atom coordinates for each token. - - Args: - coords: [B, A, 3] - rep_atom_idx: [B, L] int64 - - Returns: - [B, L, 3] - """ - idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) - return torch.gather(coords, 1, idx) - - -def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: - """Compute local atom index within each token (vectorised). - - Atoms belonging to the same token are contiguous, so this computes a - running count that resets at each token boundary. - - Args: - atom_to_token: [B, A] flat index mapping each atom to its token. - - Returns: - [B, A] tensor with values in [0, max_atoms_per_token - 1]. - """ - same_as_prev = F.pad(atom_to_token[:, 1:] == atom_to_token[:, :-1], (1, 0), value=False) - ones = torch.ones_like(atom_to_token) - cumsum = torch.cumsum(ones, dim=-1) - group_start = cumsum.masked_fill(same_as_prev, 0) - group_start = torch.cummax(group_start, dim=-1).values - return cumsum - group_start - - -def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: - """Expected value of a categorical distribution over evenly-spaced bins. - - Equivalent to ``CategoricalMixture(logits, bins=logits.shape[-1], start, end).mean()``. - - Args: - logits: [..., n_bins] - start: left boundary - end: right boundary - - Returns: - [...] expected value - """ - n_bins = logits.shape[-1] - edges = torch.linspace(start, end, n_bins + 1, device=logits.device, dtype=torch.float32) - v_bins = (edges[:-1] + edges[1:]) / 2 # [n_bins] - return (logits.float().softmax(-1) @ v_bins.unsqueeze(1)).squeeze(-1) - - -# =========================================================================== -# fp32-compute LayerNorm -# =========================================================================== - - -class ESMFold2LayerNorm(nn.LayerNorm): - """LayerNorm that always computes in fp32, with its weight stored at the model dtype. - - ESMFold2 runs in the loaded dtype (bf16 for inference) but its LayerNorms must match - the reference's fp32 norm *compute* (the fork upcasts via autocast). Keeping the weight - at the model dtype means ``from_pretrained(dtype=bf16)`` rounds it to bf16 exactly like - the reference's bf16-trained norm weights, while this ``forward`` upcasts to fp32 for - the computation and casts the result back — so no post-load weight fix-up is needed - (an fp32 load keeps full-precision weights and an fp32 compute, both no-ops here). - """ - - def forward(self, x: Tensor) -> Tensor: - weight = self.weight.float() if self.weight is not None else None - bias = self.bias.float() if self.bias is not None else None - return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) - - -# =========================================================================== -# ESMFold2TransitionLayer (used in ESMFold2DiffusionConditioning) -# =========================================================================== - - -class ESMFold2TransitionLayer(nn.Module): - """ESMFold2SwiGLU transition: norm -> a_proj, b_proj -> silu(a)*b -> out_proj.""" - - def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: - super().__init__() - hidden = n * d_model - self.norm = ESMFold2LayerNorm(d_model, eps=eps) - self.a_proj = nn.Linear(d_model, hidden, bias=False) - self.b_proj = nn.Linear(d_model, hidden, bias=False) - self.out_proj = nn.Linear(hidden, d_model, bias=False) - - def forward(self, x: Tensor) -> Tensor: - x = self.norm(x) - a = self.a_proj(x) - b = self.b_proj(x) - return self.out_proj(F.silu(a) * b) - - -# =========================================================================== -# ESMFold2AdaptiveLayerNorm (used in ESMFold2DiffusionTransformer) -# =========================================================================== - - -class ESMFold2AdaptiveLayerNorm(nn.Module): - """Adaptive layer normalization (adaLN-Zero).""" - - def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: - super().__init__() - self.d_model = d_model - self.d_cond = d_cond - self.eps = eps - self.s_scale = nn.Parameter(torch.empty(d_cond)) # ones-init in _init_weights - self.s_gate = nn.Linear(d_cond, d_model, bias=True) - self.s_shift = nn.Linear(d_cond, d_model, bias=False) - - def forward(self, a: Tensor, s: Tensor) -> Tensor: - a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) - s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) - # gate/shift in bf16 (matches the reference's autocast); a_norm is fp32 so the - # affine promotes to fp32, then downcast for the next op. - gate = torch.sigmoid(self.s_gate(s_norm)) - shift = self.s_shift(s_norm) - return (gate * a_norm + shift).to(a.dtype) - - -# =========================================================================== -# ESMFold2FourierEmbedding -# =========================================================================== - - -class ESMFold2FourierEmbedding(nn.Module): - """Fourier embedding: cos(2*pi*(t*w + b)).""" - - w: Tensor - b: Tensor - - def __init__(self, c: int) -> None: - super().__init__() - self.c = c - self.register_buffer("w", torch.randn(c)) - self.register_buffer("b", torch.randn(c)) - - def forward(self, t_hat: Tensor) -> Tensor: - # w/b are kept fp32 (ESMFold2Model._keep_in_fp32_modules_strict), so the random - # frequencies/phases — and the cos embedding — are computed at full precision. - t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) - return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) - - -# =========================================================================== -# ESMFold2SwiGLU / ESMFold2SwiGLUMLP -# =========================================================================== - - -class ESMFold2SwiGLU(nn.Module): - """ESMFold2SwiGLU with packed w12 and output w3.""" - - def __init__( - self, - in_features: int, - hidden_features: int, - out_features: int | None = None, - bias: bool = True, - ) -> None: - super().__init__() - out_features = out_features or in_features - self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) - self.w3 = nn.Linear(hidden_features, out_features, bias=bias) - self.hidden_features = hidden_features - - def forward(self, x: Tensor) -> Tensor: - x12 = self.w12(x) - x1, x2 = x12.split(self.hidden_features, dim=-1) - hidden = F.silu(x1) * x2 - return self.w3(hidden) - - -class ESMFold2SwiGLUMLP(ESMFold2SwiGLU): - """ESMFold2SwiGLU MLP with packed weights, no bias.""" - - def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: - hidden = expansion_ratio * d_model - super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) - - -# =========================================================================== -# SWA Atom Attention components -# =========================================================================== - - -def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: - """Apply RoPE with batch-dependent cos/sin. - - Args: - x: [B, L, H, D] - cos: [B, L, D/2] - sin: [B, L, D/2] - """ - ro_dim = cos.shape[-1] * 2 - cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) - sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) - return torch.cat( - [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], - dim=-1, - ) - - -@torch.compiler.disable -def build_3d_rope( - ref_pos: Tensor, - ref_space_uid: Tensor, - head_dim: int, - n_spatial_per_axis: int = 4, - n_uid_pairs: int = 2, - spatial_base_freq: float = 10000.0, - uid_base_freq: float = 10.0, -) -> tuple[Tensor, Tensor]: - """Build cos/sin for 3D RoPE + UID RoPE.""" - device = ref_pos.device - B, N = ref_pos.shape[:2] - half_dim = head_dim // 2 - n_spatial_total = 3 * n_spatial_per_axis - - spatial_inv_freq = 1.0 / ( - spatial_base_freq - ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) - ) - uid_inv_freq = 1.0 / ( - uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) - ) - - pos_f32 = ref_pos.float() - spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) - spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) - - uid_f32 = ref_space_uid.float() - uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) - - n_active = n_spatial_total + n_uid_pairs - freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) - - if n_active < half_dim: - padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) - freqs = torch.cat([freqs, padding], dim=-1) - - cos = freqs.cos().to(torch.bfloat16) - sin = freqs.sin().to(torch.bfloat16) - return cos, sin - - -def qk_norm(x: Tensor) -> Tensor: - return F.rms_norm(x, (x.size(-1),)) - - -# =========================================================================== -# ESMFold2SwiGLUFFN (atom transformer blocks) -# =========================================================================== - - -class ESMFold2SwiGLUFFN(nn.Module): - """ESMFold2SwiGLU FFN with rounded hidden size for hardware alignment.""" - - def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: - super().__init__() - hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 - self.w_up = nn.Linear(d_model, 2 * hidden_size, bias=False) - self.w_down = nn.Linear(hidden_size, d_model, bias=False) - - def forward(self, x: Tensor) -> Tensor: - x = x.to(self.w_up.weight.dtype) - x1, x2 = self.w_up(x).chunk(2, dim=-1) - return self.w_down(F.silu(x1) * x2) - - -# =========================================================================== -# ESMFold2SWA3DRoPEAttention -# =========================================================================== - - -class ESMFold2SWA3DRoPEAttention(nn.Module): - """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. - - The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention - interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), - with the sliding window expressed as an additive attention mask. The custom - flash-attention path (native bidirectional ``window_size``, plus varlen for - packed inputs) is kept as an opt-in backend, selected when - ``_attn_implementation == "flash_attention_2"``. ``config`` is attached by - the parent ``ESMFold2Model`` after construction; it is ``None`` (→ ``sdpa``) - when the module is used standalone. - """ - - def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: - super().__init__() - self.config = None - self.n_heads = n_heads - self.head_dim = d_model // n_heads - self.scale = self.head_dim**-0.5 - self.half_window = half_window - # No grouped-query attention; identity repeat keeps the interface happy. - self.num_key_value_groups = 1 - # Bidirectional encoder: never let the sdpa/flash interface default to - # causal masking when attention_mask happens to be None. - self.is_causal = False - - self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) - self.out_proj = nn.Linear(d_model, d_model, bias=False) - self.gate_proj = nn.Linear(d_model, d_model, bias=False) - - def forward(self, x: Tensor, attention_params: tuple) -> Tensor: - B, N = x.shape[:2] - cos, sin = attention_params[0], attention_params[1] - - x_input = x - qkv = self.Wqkv(x) - qkv = qkv.view(B, N, 3, self.n_heads, self.head_dim).permute(2, 0, 1, 3, 4) - q, k, v = qkv.unbind(0) - q, k = qk_norm(q), qk_norm(k) - - q = apply_rotary_emb_3d(q, cos, sin) - k = apply_rotary_emb_3d(k, cos, sin) - - input_dtype = q.dtype - if q.dtype not in (torch.float16, torch.bfloat16): - q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() - - attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" - use_flash = attn_impl == "flash_attention_2" and is_flash_attn_2_available() - - if use_flash and len(attention_params) > 2: - indices, cu_seqlens, max_seqlen = ( - attention_params[2], - attention_params[3], - attention_params[4], - ) - q_unpad = index_first_axis(q.reshape(-1, self.n_heads, self.head_dim), indices) - k_unpad = index_first_axis(k.reshape(-1, self.n_heads, self.head_dim), indices) - v_unpad = index_first_axis(v.reshape(-1, self.n_heads, self.head_dim), indices) - out_unpad = flash_attn_varlen_func( - q_unpad, - k_unpad, - v_unpad, - cu_seqlens, - cu_seqlens, - max_seqlen, - max_seqlen, - softmax_scale=self.scale, - window_size=(self.half_window, self.half_window), - ) - out = pad_input(out_unpad, indices, B, N) - elif use_flash: - out = flash_attn_func( - q, - k, - v, - softmax_scale=self.scale, - window_size=(self.half_window, self.half_window), - ) - else: - if len(attention_params) > 2: - valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) - valid[attention_params[2]] = True - valid = valid.view(B, N) - else: - valid = torch.ones(B, N, dtype=torch.bool, device=q.device) - rank = torch.cumsum(valid, dim=1) - 1 - within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window - allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) - allowed |= torch.eye(N, dtype=torch.bool, device=q.device) - # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. - attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) - attn_mask = attn_mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(q.dtype).min) - - attention_interface: Callable = eager_attention_forward - if attn_impl != "eager": - attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) - out, _ = attention_interface( - self, - q.transpose(1, 2), - k.transpose(1, 2), - v.transpose(1, 2), - attn_mask, - dropout=0.0, - scaling=self.scale, - ) - out = out * valid.unsqueeze(-1).unsqueeze(-1) - - out = out.to(input_dtype).reshape(B, N, -1) - out = out * torch.sigmoid(self.gate_proj(x_input)) - return self.out_proj(out) - - -# =========================================================================== -# ESMFold2SWAAtomBlock, ESMFold2SWAAtomTransformer -# =========================================================================== - - -def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: - return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift - - -def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: - return x + gate * y - - -class ESMFold2SWAAtomBlock(nn.Module): - """adaLN-Zero + SWA attention + ESMFold2SwiGLU FFN. - - Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight - """ - - def __init__( - self, - d_atom: int, - n_heads: int, - half_window: int = 64, - expansion_ratio: int = 2, - ) -> None: - super().__init__() - # adaln-Zero gate; zero-init lives in ESMFold2PreTrainedModel._init_weights. - self.adaln_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_atom, 6 * d_atom, bias=False)) - - self.attn = ESMFold2SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) - self.ffn = ESMFold2SwiGLUFFN(d_atom, expansion_ratio) - - def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: - mod = self.adaln_modulation(c_l) - if mod.dim() == 2: - mod = mod.unsqueeze(1) - shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) - - attn_input = _rms_adaln(x, scale_a, shift_a) - attn_out = self.attn(attn_input, attention_params) - x = _gated_residual(x, gate_a, attn_out) - - ffn_input = _rms_adaln(x, scale_f, shift_f) - ffn_out = self.ffn(ffn_input) - x = _gated_residual(x, gate_f, ffn_out) - return x - - -class ESMFold2SWAAtomTransformer(nn.Module): - """Stack of SWAAtomBlocks.""" - - def __init__( - self, - d_atom: int = 128, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.swa_window_size = swa_window_size - self.head_dim = d_atom // n_heads - self.spatial_rope_base_frequency = spatial_rope_base_frequency - self.n_spatial_rope_pairs_per_axis = n_spatial_rope_pairs_per_axis - self.n_uid_rope_pairs = n_uid_rope_pairs - self.uid_rope_base_frequency = uid_rope_base_frequency - - self.blocks = nn.ModuleList( - [ - ESMFold2SWAAtomBlock( - d_atom=d_atom, - n_heads=n_heads, - half_window=swa_window_size // 2, - expansion_ratio=expansion_ratio, - ) - for _ in range(n_blocks) - ] - ) - - def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: - return build_3d_rope( - ref_pos=ref_pos, - ref_space_uid=ref_space_uid, - head_dim=self.head_dim, - n_spatial_per_axis=self.n_spatial_rope_pairs_per_axis, - n_uid_pairs=self.n_uid_rope_pairs, - spatial_base_freq=self.spatial_rope_base_frequency, - uid_base_freq=self.uid_rope_base_frequency, - ) - - def forward( - self, - q_l: Tensor, - c_l: Tensor, - attention_params: tuple, - return_intermediates: bool = False, - ) -> Tensor | tuple[Tensor, list[Tensor]]: - intermediates: list[Tensor] = [] - for block in self.blocks: - q_l = block(q_l, c_l, attention_params) - if return_intermediates: - intermediates.append(q_l) - if return_intermediates: - return q_l, intermediates - return q_l - - -# =========================================================================== -# ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) -# =========================================================================== - - -class ESMFold2AtomEncoder(nn.Module): - """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. - - Args: - d_atom: atom hidden dim - d_token: token dim for atom_to_token aggregation - n_blocks, n_heads, swa_window_size, expansion_ratio: transformer params - structure_prediction: if True, creates coords_linear and uses full d_token - spatial_rope_base_frequency, n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs, uid_rope_base_frequency: 3D RoPE config - """ - - def __init__( - self, - d_atom: int = 128, - d_token: int = 768, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - structure_prediction: bool = True, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.d_atom = d_atom - self.d_token = d_token - self.structure_prediction = structure_prediction - - self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) - self.atom_norm = ESMFold2LayerNorm(d_atom) - - if structure_prediction: - self.coords_linear = nn.Linear(6, d_atom, bias=False) - - self.atom_transformer = ESMFold2SWAAtomTransformer( - d_atom=d_atom, - n_blocks=n_blocks, - n_heads=n_heads, - swa_window_size=swa_window_size, - expansion_ratio=expansion_ratio, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - # Output aggregation: d_token for structure prediction, d_token//2 for inputs - out_dim = d_token if structure_prediction else d_token // 2 - self.atom_to_token_linear = nn.Linear(d_atom, out_dim, bias=False) - - def forward( - self, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, - r_l: Tensor | None = None, - pred_r1: Tensor | None = None, - num_diffusion_samples: int = 1, - return_intermediates: bool = False, - inference_cache: dict | None = None, - ) -> tuple[Tensor, Tensor, Tensor, tuple, list[Tensor]]: - """Returns (a, q, c, attention_params, intermediates). - - ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, - attention indices, n_tokens) across diffusion steps. - """ - B, N = ref_pos.shape[:2] - - layer_cache = None - if inference_cache is not None: - layer_cache = inference_cache.setdefault("atomencoder", {}) - - if layer_cache is None or len(layer_cache) == 0: - atom_feats = torch.cat( - [ - ref_pos, - ref_charge.unsqueeze(-1), - atom_attention_mask.unsqueeze(-1), - ref_element, - ref_atom_name_chars.reshape(B, N, MAX_CHARS * CHAR_VOCAB_SIZE), - ], - dim=-1, - ) - c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( - self.atom_linear.weight.dtype - ) - cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) - cos = cos.repeat_interleave(num_diffusion_samples, 0) - sin = sin.repeat_interleave(num_diffusion_samples, 0) - mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) - seqlens = mask_exp.sum(dim=-1, dtype=torch.int32) - indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() - max_seqlen = int(seqlens.max().item()) - cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) - attention_params = (cos, sin, indices, cu_seqlens, max_seqlen) - n_tokens = int(atom_to_token.max().item()) + 1 - if layer_cache is not None: - layer_cache["c_base"] = c_base - layer_cache["attention_params"] = attention_params - layer_cache["mask_exp"] = mask_exp - layer_cache["n_tokens"] = n_tokens - layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - else: - c_base = layer_cache["c_base"] - attention_params = layer_cache["attention_params"] - mask_exp = layer_cache["mask_exp"] - n_tokens = layer_cache["n_tokens"] - - c = c_base - - q = c - - if self.structure_prediction and r_l is not None: - q = q.repeat_interleave(num_diffusion_samples, 0) - if pred_r1 is None: - pred_r1 = torch.zeros_like(r_l) - r_input = torch.cat([r_l, pred_r1], dim=-1) - r_to_q = self.coords_linear(r_input.to(self.coords_linear.weight.dtype)) - q = q + r_to_q - - c = c.repeat_interleave(num_diffusion_samples, 0) - - result = self.atom_transformer( - q_l=q, - c_l=c, - attention_params=attention_params, - return_intermediates=return_intermediates, - ) - if return_intermediates: - q, intermediates = result - else: - q = result - intermediates = [] - - q_to_a = F.relu(self.atom_to_token_linear(q)) - if layer_cache is not None and "atom_to_token_exp" in layer_cache: - atom_to_token_exp = layer_cache["atom_to_token_exp"] - else: - atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) - - return a, q, c, attention_params, intermediates - - -# =========================================================================== -# ESMFold2AtomDecoder -# =========================================================================== - - -class ESMFold2AtomDecoder(nn.Module): - """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear.""" - - def __init__( - self, - d_atom: int = 128, - d_token: int = 768, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) - - self.atom_transformer = ESMFold2SWAAtomTransformer( - d_atom=d_atom, - n_blocks=n_blocks, - n_heads=n_heads, - swa_window_size=swa_window_size, - expansion_ratio=expansion_ratio, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - self.norm = ESMFold2LayerNorm(d_atom) - self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) - - def forward( - self, - a_i: Tensor, - q_l: Tensor, - c_l: Tensor, - p_lm: tuple, - atom_to_token: Tensor, - atom_attention_mask: Tensor, - num_diffusion_samples: int = 1, - return_intermediates: bool = False, - ) -> tuple[Tensor, list[Tensor]]: - """Returns (r_update, intermediates).""" - atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - a_to_q = self.token_to_atom_linear(a_i) - a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) - q_l = q_l + a_to_q - - result = self.atom_transformer( - q_l=q_l, - c_l=c_l, - attention_params=p_lm, - return_intermediates=return_intermediates, - ) - if return_intermediates: - q_l, intermediates = result - else: - q_l = result - intermediates = [] - - r_l = self.output_linear(self.norm(q_l)) - return r_l, intermediates - - -# =========================================================================== -# ESMFold2AttentionPairBias (ESMFold2DiffusionTransformer attention block) -# =========================================================================== - - -class ESMFold2AttentionPairBias(nn.Module): - """Gated multi-head attention with pair bias conditioning.""" - - def __init__( - self, - d_model: int, - d_pair: int, - num_heads: int, - d_cond: int | None = None, - use_conditioning: bool = True, - ) -> None: - super().__init__() - self.d_model = d_model - self.num_heads = num_heads - self.head_dim = d_model // num_heads - self.scale = self.head_dim**-0.5 - d_cond = d_cond or d_model - - if use_conditioning: - self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) - # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. - self.out_gate = nn.Linear(d_cond, d_model, bias=True) - else: - self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) - - self.q_proj = nn.Linear(d_model, d_model, bias=True) - self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) - self.g_proj = nn.Linear(d_model, d_model, bias=False) - self.out_proj = nn.Linear(d_model, d_model, bias=False) - - if d_pair > 0: - self.pair_norm = ESMFold2LayerNorm(d_pair, eps=1e-5) - self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) - - def compute_pair_bias(self, z: Tensor, bsz: int, num_diffusion_samples: int = 1) -> Tensor: - """Project the (normed) pair representation to per-head attention biases. - - Depends only on ``z`` and this block's fixed weights, so it is invariant - across diffusion sampling steps — the sampler computes it once and reuses - it (see ``ESMFold2DiffusionTransformer.forward``). Bit-identical to computing it - inline every step. - """ - if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: - z = z.repeat_interleave(num_diffusion_samples, dim=0) - if z.dim() == 4: - return self.pair_bias_proj(self.pair_norm(z)) - return z.unsqueeze(-1) - - def forward( - self, - a: Tensor, - s: Tensor | None, - z: Tensor, - attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - pair_bias: Tensor | None = None, - ) -> Tensor: - bsz, n_queries, d_model = a.shape - - if s is not None: - x = self.adaln(a, s) - else: - x = self.pre_norm(a) - - n_keys = x.shape[1] - q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) - kv = self.kv_proj(x) - k, v = kv.chunk(2, dim=-1) - k = k.view(bsz, n_keys, self.num_heads, self.head_dim) - v = v.view(bsz, n_keys, self.num_heads, self.head_dim) - - if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: - attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) - - # Standard attention with pair bias - g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) - - logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale - - # ``pair_bias`` is step-invariant; the diffusion sampler precomputes and - # caches it across steps. Compute inline when not supplied (e.g. uncached). - if pair_bias is None: - pair_bias = self.compute_pair_bias(z, bsz, num_diffusion_samples) - logits = logits + pair_bias.to(dtype=logits.dtype) - - if attention_mask is not None: - min_val = torch.finfo(logits.dtype).min - mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) - logits = logits + mask_bias.to(dtype=logits.dtype) - - attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) - ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) - ctx = g * ctx - out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) - - if s is not None: - out = torch.sigmoid(self.out_gate(s)) * out - return out - - -# =========================================================================== -# ESMFold2ConditionedTransitionBlock -# =========================================================================== - - -class ESMFold2ConditionedTransitionBlock(nn.Module): - """Conditioned ESMFold2SwiGLU transition with adaptive layer norm.""" - - def __init__( - self, - d_model: int, - d_cond: int | None = None, - transition_multiplier: int = 2, - use_conditioning: bool = True, - ) -> None: - super().__init__() - d_cond = d_cond or d_model - hidden = transition_multiplier * d_model - - if use_conditioning: - self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) - # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. - self.output_gate = nn.Linear(d_cond, d_model, bias=True) - else: - self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) - - self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) - self.lin_out = nn.Linear(hidden, d_model, bias=False) - - def forward(self, a: Tensor, s: Tensor | None) -> Tensor: - if s is not None: - x = self.adaln(a, s) - else: - x = self.pre_norm(a) - - swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) - b = F.silu(swish_a) * swish_b - out = self.lin_out(b) - - if s is not None: - out = torch.sigmoid(self.output_gate(s)) * out - return out - - -# =========================================================================== -# ESMFold2DiffusionTransformer (token transformer) -# =========================================================================== - - -class ESMFold2DiffusionTransformer(nn.Module): - """Diffusion denoising transformer with attention pair bias.""" - - def __init__( - self, - d_model: int, - d_pair: int, - num_heads: int, - num_blocks: int, - d_cond: int | None = None, - transition_multiplier: int = 2, - use_conditioning: bool = True, - ) -> None: - super().__init__() - d_cond = d_cond or d_model - - self.attn_blocks = nn.ModuleList( - [ - ESMFold2AttentionPairBias( - d_model=d_model, - d_pair=d_pair, - num_heads=num_heads, - d_cond=d_cond, - use_conditioning=use_conditioning, - ) - for _ in range(num_blocks) - ] - ) - self.transition_blocks = nn.ModuleList( - [ - ESMFold2ConditionedTransitionBlock( - d_model=d_model, - d_cond=d_cond, - transition_multiplier=transition_multiplier, - use_conditioning=use_conditioning, - ) - for _ in range(num_blocks) - ] - ) - - def forward( - self, - a: Tensor, - s: Tensor | None, - z: Tensor, - attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - return_intermediates: bool = False, - inference_cache: dict | None = None, - ) -> tuple[Tensor, list[Tensor]]: - intermediates: list[Tensor] = [] - x = a - bsz = a.shape[0] - # Each block's pair bias depends only on the (step-invariant) conditioning - # pair ``z`` and fixed weights, so compute it once per block and reuse it - # across every diffusion sampling step. Bit-identical to recomputing it - # each step; the cache lives in the sampler's per-fold ``inference_cache``. - bias_cache = None if inference_cache is None else inference_cache.setdefault("token_pair_bias", {}) - for i, (attn, transition) in enumerate(zip(self.attn_blocks, self.transition_blocks)): - if bias_cache is None: - pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) - elif i in bias_cache: - pair_bias = bias_cache[i] - else: - pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) - bias_cache[i] = pair_bias - x = x + attn( - x, - s, - z, - attention_mask=attention_mask, - num_diffusion_samples=num_diffusion_samples, - pair_bias=pair_bias, - ) - x = x + transition(x, s) - if return_intermediates: - intermediates.append(x) - return x, intermediates - - -# =========================================================================== -# ESMFold2DiffusionConditioning -# =========================================================================== - - -class ESMFold2DiffusionConditioning(nn.Module): - """Conditions pair and single representations on noise timestep.""" - - def __init__( - self, - c_z: int = 256, - c_s: int = 768, - c_s_inputs: int = 451, - sigma_data: float = 16.0, - fourier_dim: int = 256, - transition_multiplier: int = 2, - layer_norm_eps: float = 1e-5, - ) -> None: - super().__init__() - self.sigma_data = float(sigma_data) - self.c_z = c_z - self.c_s = c_s - self.c_s_inputs = c_s_inputs - - self.z_input_norm = ESMFold2LayerNorm(2 * c_z, eps=layer_norm_eps) - self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) - self.z_transitions = nn.ModuleList( - [ESMFold2TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] - ) - - self.s_input_norm = ESMFold2LayerNorm(c_s_inputs, eps=layer_norm_eps) - self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) - self.fourier = ESMFold2FourierEmbedding(fourier_dim) - self.noise_norm = ESMFold2LayerNorm(fourier_dim, eps=layer_norm_eps) - self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) - self.s_transitions = nn.ModuleList( - [ESMFold2TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] - ) - - def forward( - self, - t_hat: Tensor, - s_inputs: Tensor, - z_trunk: Tensor, - relative_position_encoding: Tensor, - sigma_data: float | None = None, - num_diffusion_samples: int = 1, - inference_cache: dict[str, Tensor] | None = None, - ) -> tuple[Tensor, Tensor]: - sigma = self.sigma_data if sigma_data is None else float(sigma_data) - base_batch = z_trunk.shape[0] - target_batch = base_batch * num_diffusion_samples - - # z conditioning (cached across diffusion steps — independent of t_hat) - if inference_cache is not None and "z" in inference_cache: - z = inference_cache["z"] - else: - z_rel = relative_position_encoding.to(dtype=torch.float32) - z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) - # The relpos/coords conditioning is fp32; z_input_norm keeps it fp32, - # then we hand off to z_proj in the model's compute dtype. - z = self.z_proj(self.z_input_norm(z).to(self.z_proj.weight.dtype)) - for block in self.z_transitions: - z = z + block(z) - if inference_cache is not None: - inference_cache["z"] = z - - # s conditioning - s_inputs_eff = s_inputs - if s_inputs_eff.shape[0] != target_batch: - s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) - - s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype)) - - # Noise embedding - t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) - if t.numel() == 1: - t = t.expand(target_batch) - elif t.shape[0] != target_batch: - t = t.repeat_interleave(num_diffusion_samples, 0) - t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) - n = self.fourier(t_noise) - n = self.noise_proj(self.noise_norm(n.float()).to(self.noise_proj.weight.dtype)) - s = s + n.unsqueeze(1) - - for block in self.s_transitions: - s = s + block(s) - - return s, z - - -# =========================================================================== -# ESMFold2DiffusionModule -# =========================================================================== - - -class ESMFold2DiffusionModule(nn.Module): - """Diffusion denoising module for structure prediction.""" - - def __init__( - self, - c_atom: int = 128, - c_token: int = 768, - c_z: int = 256, - c_s_inputs: int = 451, - sigma_data: float = 16.0, - fourier_dim: int = 256, - atom_num_blocks: int = 3, - atom_num_heads: int = 4, - token_num_blocks: int = 12, - token_num_heads: int = 16, - transition_multiplier: int = 2, - swa_window_size: int = 128, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: - super().__init__() - self.sigma_data = float(sigma_data) - - self.conditioning = ESMFold2DiffusionConditioning( - c_z=c_z, - c_s=c_token, # conditioning s output is c_token - c_s_inputs=c_s_inputs, - sigma_data=sigma_data, - fourier_dim=fourier_dim, - transition_multiplier=transition_multiplier, - ) - - # Atom encoder (structure_prediction=True, with coords_linear) - self.atom_encoder = ESMFold2AtomEncoder( - d_atom=c_atom, - d_token=c_token, - n_blocks=atom_num_blocks, - n_heads=atom_num_heads, - swa_window_size=swa_window_size, - expansion_ratio=2, - structure_prediction=True, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - # Atom decoder - self.atom_decoder = ESMFold2AtomDecoder( - d_atom=c_atom, - d_token=c_token, - n_blocks=atom_num_blocks, - n_heads=atom_num_heads, - swa_window_size=swa_window_size, - expansion_ratio=2, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) - - # zero-init lives in ESMFold2PreTrainedModel._init_weights. - self.s_to_token = nn.Linear(c_token, c_token, bias=False) - - # Token transformer (ESMFold2DiffusionTransformer with pair bias) - self.token_transformer = ESMFold2DiffusionTransformer( - d_model=c_token, - d_pair=c_z, - num_heads=token_num_heads, - num_blocks=token_num_blocks, - d_cond=c_token, - transition_multiplier=transition_multiplier, - use_conditioning=True, - ) - - self.s_step_norm = ESMFold2LayerNorm(c_token) - self.token_norm = ESMFold2LayerNorm(c_token) - - def forward( - self, - x_noisy: Tensor, - t_hat: Tensor, - ref_pos: Tensor, - ref_charge: Tensor, - ref_mask: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - tok_idx: Tensor, - s_inputs: Tensor, - z_trunk: Tensor, - relative_position_encoding: Tensor, - asym_id: Tensor, - residue_index: Tensor, - entity_id: Tensor, - token_index: Tensor, - sym_id: Tensor, - sigma_data: float | None = None, - token_attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - return_atom_repr: bool = False, - inference_cache: dict[str, Tensor] | None = None, - ) -> dict[str, Tensor | None]: - bsz = x_noisy.shape[0] - sigma = self.sigma_data if sigma_data is None else float(sigma_data) - t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape(-1) - if t.numel() == 1: - t = t.expand(bsz) - - # Step 1: conditioning (pair z is cached across diffusion steps) - s, z = self.conditioning( - t_hat=t, - s_inputs=s_inputs, - z_trunk=z_trunk, - relative_position_encoding=relative_position_encoding, - sigma_data=sigma, - num_diffusion_samples=num_diffusion_samples, - inference_cache=inference_cache, - ) - - # Step 2: normalize noisy coords - denom = torch.sqrt(t * t + sigma * sigma) - r_noisy = x_noisy / denom[:, None, None] - - # Step 3: atom encoder - a, q_skip, c_skip, p_skip, enc_intermediates = self.atom_encoder( - ref_pos=ref_pos, - atom_attention_mask=ref_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=tok_idx, - r_l=r_noisy, - num_diffusion_samples=num_diffusion_samples, - return_intermediates=return_atom_repr, - inference_cache=inference_cache, - ) - - # Step 4: add conditioned s - a = a + self.s_to_token(self.s_step_norm(s)) - - # Step 5: token transformer (pair bias is cached across steps via inference_cache) - a, _ = self.token_transformer( - a, - s, - z, - attention_mask=token_attention_mask, - num_diffusion_samples=num_diffusion_samples, - inference_cache=inference_cache, - ) - - # Step 6: token norm - a = self.token_norm(a) - - # Step 7: atom decoder - r_update, dec_intermediates = self.atom_decoder( - a_i=a, - q_l=q_skip, - c_l=c_skip, - p_lm=p_skip, - atom_to_token=tok_idx, - atom_attention_mask=ref_mask, - num_diffusion_samples=num_diffusion_samples, - return_intermediates=return_atom_repr, - ) - - # Step 8: compute denoised output - sigma2 = sigma * sigma - t2 = t * t - out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy - out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update - - # Collect atom intermediates from encoder + decoder - atom_intermediates: Tensor | None = None - if return_atom_repr: - all_ints = enc_intermediates + dec_intermediates - if all_ints: - atom_intermediates = torch.stack(all_ints, dim=2) - - return { - "x_denoised": out, - "atom_intermediates": atom_intermediates, - } - - -# =========================================================================== -# ESMFold2DiffusionStructureHead -# =========================================================================== - - -class ESMFold2DiffusionStructureHead(nn.Module): - """Wrapper around ESMFold2DiffusionModule with diffusion sampling.""" - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__() - dm = config.structure_head.diffusion_module - swa_cfg = config.inputs.atom_encoder - sh = config.structure_head - - self.diffusion_module = ESMFold2DiffusionModule( - c_atom=dm.c_atom, - c_token=dm.c_token, - c_z=dm.c_z, - c_s_inputs=dm.c_s_inputs, - sigma_data=dm.sigma_data, - fourier_dim=dm.fourier_dim, - atom_num_blocks=dm.atom_num_blocks, - atom_num_heads=dm.atom_num_heads, - token_num_blocks=dm.token_num_blocks, - token_num_heads=dm.token_num_heads, - transition_multiplier=dm.transition_multiplier, - swa_window_size=swa_cfg.swa_window_size, - spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, - uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, - ) - - # Sampling hyperparameters - self.sigma_data = dm.sigma_data - self.gamma_0 = sh.gamma_0 - self.gamma_min = sh.gamma_min - self.noise_scale = sh.noise_scale - self.step_scale = sh.step_scale - self.inference_s_max = sh.inference_s_max - self.inference_s_min = sh.inference_s_min - self.inference_p = sh.inference_p - self.inference_num_steps = sh.inference_num_steps - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def inference_noise_schedule(self, num_steps: int | None = None, device: torch.device | None = None) -> Tensor: - """Karras power-law noise schedule.""" - steps = self.inference_num_steps if num_steps is None else int(num_steps) - if steps == 1: - return torch.tensor( - [self.inference_s_max * self.sigma_data, 0.0], - device=device, - dtype=torch.float32, - ) - p = float(self.inference_p) - inv_p = 1.0 / p - k = torch.arange(steps, device=device, dtype=torch.float32) - base = self.inference_s_max**inv_p + (k / (steps - 1)) * ( - self.inference_s_min**inv_p - self.inference_s_max**inv_p - ) - schedule = self.sigma_data * base.pow(p) - return F.pad(schedule, (0, 1), value=0.0) - - @staticmethod - def _random_rotations(n: int, dtype: torch.dtype, device: torch.device) -> Tensor: - q = torch.randn((n, 4), dtype=dtype, device=device) - scale = torch.sqrt((q * q).sum(dim=1)) - signs = torch.where(q[:, 0] < 0, -scale, scale) - q = q / signs[:, None] - r, i, j, k = torch.unbind(q, dim=-1) - two_s = 2.0 / (q * q).sum(dim=-1) - return torch.stack( - ( - 1 - two_s * (j * j + k * k), - two_s * (i * j - k * r), - two_s * (i * k + j * r), - two_s * (i * j + k * r), - 1 - two_s * (i * i + k * k), - two_s * (j * k - i * r), - two_s * (i * k - j * r), - two_s * (j * k + i * r), - 1 - two_s * (i * i + j * j), - ), - dim=-1, - ).reshape(n, 3, 3) - - def _center_random_augmentation( - self, x: Tensor, atom_mask: Tensor, second_coords: Tensor | None = None - ) -> tuple[Tensor, Tensor | None]: - """Algorithm 19: center + random rotation + translation.""" - bsz = x.shape[0] - mask = atom_mask.unsqueeze(-1) # [B, A, 1] - denom = mask.sum(dim=1, keepdim=True).clamp(min=1) - mean = (x * mask).sum(dim=1, keepdim=True) / denom - x = x - mean - if second_coords is not None: - second_coords = second_coords - mean - - r = self._random_rotations(bsz, x.dtype, x.device) - x = torch.einsum("bmd,bds->bms", x, r) - if second_coords is not None: - second_coords = torch.einsum("bmd,bds->bms", second_coords, r) - - t = torch.randn_like(x[:, 0:1, :]) - x = x + t - if second_coords is not None: - second_coords = second_coords + t - return x, second_coords - - @staticmethod - def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> Tensor: - """Kabsch alignment: align x to x_gt with weights w.""" - w = (mask * w).unsqueeze(-1) # [B, N, 1] - denom = w.sum(dim=-2, keepdim=True).clamp(min=1e-8) - mu = (x * w).sum(dim=-2, keepdim=True) / denom - mu_gt = (x_gt * w).sum(dim=-2, keepdim=True) / denom - x_c = x - mu - xgt_c = x_gt - mu_gt - H = torch.einsum("bni,bnj->bij", w * xgt_c, x_c) - H32 = H.float() - U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) - det = torch.linalg.det(U @ Vh) - ones = torch.ones_like(det) - R = (U @ torch.diag_embed(torch.stack([ones, ones, det], dim=-1)) @ Vh).to(H.dtype) - return x_c @ R.transpose(-1, -2) + mu_gt - - # ------------------------------------------------------------------ - # Sampling - # ------------------------------------------------------------------ - - @torch.inference_mode() - def sample( - self, - z_trunk: Tensor, - s_inputs: Tensor, - relative_position_encoding: Tensor, - ref_pos: Tensor, - ref_charge: Tensor, - ref_mask: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - tok_idx: Tensor, - asym_id: Tensor, - residue_index: Tensor, - entity_id: Tensor, - token_index: Tensor, - sym_id: Tensor, - token_attention_mask: Tensor | None = None, - num_diffusion_samples: int = 1, - num_sampling_steps: int | None = None, - max_inference_sigma: float | None = 256.0, - noise_scale: float | None = None, - step_scale: float | None = None, - return_atom_repr: bool = False, - use_inference_cache: bool = True, - denoising_early_exit_rmsd: float | None = None, - ) -> dict[str, Tensor | None]: - """Diffusion sampling (Algorithm 18). - - ``num_sampling_steps`` is the number of denoising steps actually run. - When ``max_inference_sigma`` is set, the Karras schedule built with - ``num_sampling_steps`` entries would lose its high-σ tail to the cap, - so we inflate the underlying schedule length here to land back at the - requested step count post-truncation. - """ - n_atoms = tok_idx.shape[1] - device = s_inputs.device - target_batch = s_inputs.shape[0] * num_diffusion_samples - - inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None - - steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) - - schedule = self.inference_noise_schedule(steps, device) - if max_inference_sigma is not None: - schedule = schedule[schedule <= float(max_inference_sigma)] - schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) - - lam = self.noise_scale if noise_scale is None else float(noise_scale) - eta = self.step_scale if step_scale is None else float(step_scale) - - x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) - atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() - - gammas = torch.where( - schedule > self.gamma_min, - torch.full_like(schedule, self.gamma_0), - torch.zeros_like(schedule), - ) - - x_denoised_prev: Tensor | None = None - diff_atom_intermediates: Tensor | None = None - - step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) - num_steps = len(step_pairs) - - for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): - x, x_denoised_prev = self._center_random_augmentation(x, atom_mask, second_coords=x_denoised_prev) - - sigma_tm_val = float(sigma_tm.item()) - t_hat_val = sigma_tm_val * (1.0 + float(gamma.item())) - eps_std = lam * max(t_hat_val**2 - sigma_tm_val**2, 0.0) ** 0.5 - x_noisy = x + eps_std * torch.randn_like(x) - - is_last_step = step_idx == num_steps - 1 - request_atom_repr = return_atom_repr and (is_last_step or denoising_early_exit_rmsd is not None) - - dm_out = self.diffusion_module( - x_noisy=x_noisy, - t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), - ref_pos=ref_pos, - ref_charge=ref_charge, - ref_mask=ref_mask, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - ref_space_uid=ref_space_uid, - tok_idx=tok_idx, - s_inputs=s_inputs, - z_trunk=z_trunk, - relative_position_encoding=relative_position_encoding, - asym_id=asym_id, - residue_index=residue_index, - entity_id=entity_id, - token_index=token_index, - sym_id=sym_id, - token_attention_mask=token_attention_mask, - num_diffusion_samples=num_diffusion_samples, - return_atom_repr=request_atom_repr, - inference_cache=inference_cache, - ) - - x_denoised = dm_out["x_denoised"] - if request_atom_repr: - diff_atom_intermediates = dm_out.get("atom_intermediates") - - # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts - # to fp32 internally for the SVD/det. - x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) - x_noisy = x_noisy.to(dtype=x_denoised.dtype) - - # ODE/SDE step - sigma_t_val = float(sigma_t.item()) - denoised_over_sigma = (x_noisy - x_denoised) / t_hat_val - x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma - - # Denoising early-exit: stop when consecutive predictions converge - if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: - aligned = self._weighted_rigid_align( - x_denoised_prev.float(), - x_denoised.float(), - atom_mask, - atom_mask, - ) - diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) - per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() - if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: - x = x_denoised - x_denoised_prev = x_denoised - break - - x_denoised_prev = x_denoised - - result: dict[str, Tensor | None] = { - "sample_atom_coords": x, - } - if return_atom_repr: - result["diff_atom_intermediates"] = diff_atom_intermediates - return result - - -# =========================================================================== -# ESMFold2RowAttentionPooling -# =========================================================================== - - -class ESMFold2RowAttentionPooling(nn.Module): - """Row-wise attention pooling: attn_proj, out_proj.""" - - def __init__(self, d_pair: int, d_single: int) -> None: - super().__init__() - self.attn_proj = nn.Linear(d_pair, 1, bias=False) - self.out_proj = nn.Linear(d_pair, d_single, bias=False) - - def forward(self, z: Tensor, mask: Tensor) -> Tensor: - scores = self.attn_proj(z).squeeze(-1) - mask_bias = torch.where( - mask[:, None, :].bool(), - torch.zeros_like(scores), - torch.full_like(scores, -1e9), - ) - scores = scores + mask_bias - weights = F.softmax(scores, dim=-1, dtype=torch.float32).to(scores.dtype) - pooled = torch.einsum("bnm,bnmd->bnd", weights, z) - return self.out_proj(pooled) - - -# =========================================================================== -# ESMFold2InputsEmbedder -# =========================================================================== - - -class ESMFold2InputsEmbedder(nn.Module): - """Embeds input features including atom-level encoding via SWA attention.""" - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__() - swa_cfg = config.inputs.atom_encoder - - self.atom_attention_encoder = ESMFold2AtomEncoder( - d_atom=swa_cfg.d_atom, - d_token=swa_cfg.d_token, - n_blocks=swa_cfg.n_blocks, - n_heads=swa_cfg.n_heads, - swa_window_size=swa_cfg.swa_window_size, - expansion_ratio=swa_cfg.expansion_ratio, - structure_prediction=False, # no coords_linear - spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, - uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, - ) - - def forward( - self, - aatype: Tensor, - profile: Tensor, - deletion_mean: Tensor, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, - ) -> Tensor: - """Embed inputs into per-token features. - - Returns: - [B, L, d_inputs] concatenation of atom encoding, aatype, profile, - and deletion_mean. - """ - a, _q, _c, _attn_params, _intermediates = self.atom_attention_encoder( - ref_pos=ref_pos, - atom_attention_mask=atom_attention_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=atom_to_token, - ) - # The continuous input features are fp32; fold them into the atom - # encoding's (compute) dtype so the single representation is one dtype. - dtype = a.dtype - return torch.cat( - [a, aatype.to(dtype), profile.to(dtype), deletion_mean.unsqueeze(-1).to(dtype)], - dim=-1, - ) - - -# =========================================================================== -# ESMFold2ResIdxAsymIdSymIdEntityIdEncoding (trunk relative position) -# =========================================================================== - - -class ESMFold2ResIdxAsymIdSymIdEntityIdEncoding(nn.Module): - """embed.weight [d_pair, n_features] where n_features = 2*(2*r_bins+2) + 1 + (2*c_bins+2). - - For default r_bins=32, c_bins=2: 2*66 + 1 + 6 = 139. - """ - - def __init__( - self, - n_relative_residx_bins: int = 32, - n_relative_chain_bins: int = 2, - d_pair: int = 256, - ) -> None: - super().__init__() - self.n_relative_residx_bins = n_relative_residx_bins - self.n_relative_chain_bins = n_relative_chain_bins - self.d_pair = d_pair - - n_feats_residue = 2 * n_relative_residx_bins + 2 - n_feats_token = 2 * n_relative_residx_bins + 2 - n_feats_chain = 2 * n_relative_chain_bins + 2 - n_feats_same_entity = 1 - total_feats = n_feats_residue + n_feats_token + n_feats_chain + n_feats_same_entity - self.embed = nn.Linear(total_feats, d_pair, bias=False) - - def forward( - self, - residue_index: Tensor, - asym_id: Tensor, - sym_id: Tensor, - entity_id: Tensor, - token_index: Tensor, - ) -> Tensor: - bij_same_chain = asym_id.unsqueeze(2) == asym_id.unsqueeze(1) - bij_same_residue = residue_index.unsqueeze(2) == residue_index.unsqueeze(1) - bij_same_entity = entity_id.unsqueeze(2) == entity_id.unsqueeze(1) - - dij_residue = residue_index.unsqueeze(2) - residue_index.unsqueeze(1) - dij_residue = torch.clip( - dij_residue + self.n_relative_residx_bins, - 0, - 2 * self.n_relative_residx_bins, - ) - dij_residue = torch.where(bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1) - aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) - - dij_token = torch.clip( - token_index.unsqueeze(2) - token_index.unsqueeze(1) + self.n_relative_residx_bins, - 0, - 2 * self.n_relative_residx_bins, - ) - dij_token = torch.where( - bij_same_chain & bij_same_residue, - dij_token, - 2 * self.n_relative_residx_bins + 1, - ) - aij_rel_token = F.one_hot(dij_token, 2 * self.n_relative_residx_bins + 2) - - dij_chain = torch.clip( - sym_id.unsqueeze(2) - sym_id.unsqueeze(1) + self.n_relative_chain_bins, - 0, - 2 * self.n_relative_chain_bins, - ) - dij_chain = torch.where(bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain) - aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) - - feats = torch.cat( - [ - aij_rel_pos.float(), - aij_rel_token.float(), - bij_same_entity.float().unsqueeze(-1), - aij_rel_chain.float(), - ], - dim=-1, - ) - - return self.embed(feats.to(self.embed.weight.dtype)) - - -# =========================================================================== -# ESMFold2SingleToPair (for ESMFold2LanguageModelShim) -# =========================================================================== - - -class ESMFold2SingleToPair(nn.Module): - """downproject, output_mlp (Sequential of Linear, GELU, Linear).""" - - def __init__(self, input_dim: int, downproject_dim: int, output_dim: int) -> None: - super().__init__() - self.downproject = nn.Linear(input_dim, downproject_dim) - self.output_mlp = nn.Sequential( - nn.Linear(2 * downproject_dim, output_dim), - nn.GELU(), - nn.Linear(output_dim, output_dim), - ) - - def forward(self, x: Tensor) -> Tensor: - x = self.downproject(x) - x = torch.cat( - [(x.unsqueeze(2) * x.unsqueeze(1)), (x.unsqueeze(2) - x.unsqueeze(1))], - dim=3, - ) - return self.output_mlp(x) - - -# =========================================================================== -# ESMFold2LanguageModelShim -# =========================================================================== - - -class ESMFold2LanguageModelShim(nn.Module): - """Shim holding the trainable projection weights for LM integration. - - Contains: - - base_z_combine: nn.Parameter [num_layers+1] - - base_z_linear: Sequential(ESMFold2LayerNorm(d_model), Linear(d_model, d_z, bias=False)) - - base_z_mlp: Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) - """ - - def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: - super().__init__() - - self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) - self.base_z_linear = nn.Sequential(ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) - self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) - - def forward(self, hidden_states: Tensor) -> Tensor: - """Project pre-computed ESMC hidden states to pair representation. - - Args: - hidden_states: [B, L, num_layers+1, d_model] from ESMC 6B. - - Returns: - [B, L, L, d_pair] pair representation. - """ - # The ESMC backbone may be loaded at a different precision than the trunk - # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. - hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) - # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. - normed = self.base_z_linear[0](hidden_states) - lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] - weights = self.base_z_combine.softmax(0) # [81] - lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] - # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. - pair = self.base_z_mlp[0](lm_z) - lm_z = self.base_z_mlp[1](pair) # [B, L, L, d_z] - return lm_z - - -# =========================================================================== -# ESMFold2 — language-model backbone helpers -# =========================================================================== - - -def compute_lm_hidden_states( - esmc: nn.Module, - input_ids: Tensor, - asym_id: Tensor, - residue_index: Tensor, - mol_type: Tensor, - token_mask: Tensor, - pad_to_multiple: int | None = None, -) -> Tensor: - """Run ESMC with BOS/EOS wrapping, return hidden states [B, L, N, D] with N=81 layers. - - Atom-tokenized modified residues (HYP, MSE, ACE, NH2, ...) span multiple - structure tokens but share a single ``(asym_id, residue_index)`` key — - collapse them to one LM token per residue before running the LM (the LM - was trained on per-residue inputs, not per-atom), then scatter the - hidden states back to the per-token layout. - """ - B, L = input_ids.shape - device = input_ids.device - protein_mask = (mol_type == 0) & token_mask - - lm_input_list = [] - lm_lengths = [] - # Per-batch maps from (original protein-token index) to (LM input position). - expand_maps: list[Tensor] = [] - for b in range(B): - mask_b = protein_mask[b] - ids_b = input_ids[b][mask_b] - asym_b = asym_id[b][mask_b] - res_b = residue_index[b][mask_b] - - # Collapse: keep first token per (asym_id, residue_index) key, in - # input order. ``inverse`` maps each original protein-token to its - # collapsed residue index. - keys = torch.stack((asym_b, res_b), dim=1) - unique_keys, inverse = torch.unique(keys, dim=0, return_inverse=True) - n_unique = unique_keys.size(0) - token_positions = torch.arange(keys.size(0), device=device, dtype=torch.long) - first_pos = torch.full((n_unique,), keys.size(0), device=device, dtype=torch.long) - first_pos.scatter_reduce_(0, inverse, token_positions, reduce="amin", include_self=True) - ordered = torch.argsort(first_pos) - first_pos_ordered = first_pos[ordered] - ids_collapsed = ids_b[first_pos_ordered] - asym_collapsed = asym_b[first_pos_ordered] - remap = torch.empty_like(ordered) - remap[ordered] = torch.arange(n_unique, device=device, dtype=torch.long) - inverse_ordered = remap[inverse] - - chain_ids = asym_collapsed.unique(sorted=True) - # [BOS] chain1 [EOS BOS] chain2 ... [EOS] - parts: list[Tensor] = [torch.tensor([0], device=device, dtype=ids_b.dtype)] - # Per-chain LM positions accumulate; track them for the expand map. - per_token_lm_pos = torch.empty(n_unique, device=device, dtype=torch.long) - cursor = 1 # position 0 is the leading BOS - for i, cid in enumerate(chain_ids): - in_chain = (asym_collapsed == cid).nonzero(as_tuple=True)[0] - parts.append(ids_collapsed[in_chain]) - per_token_lm_pos[in_chain] = torch.arange( - cursor, cursor + in_chain.shape[0], device=device, dtype=torch.long - ) - cursor += in_chain.shape[0] - if i < len(chain_ids) - 1: - parts.append(torch.tensor([2, 0], device=device, dtype=ids_b.dtype)) - cursor += 2 # EOS + BOS - parts.append(torch.tensor([2], device=device, dtype=ids_b.dtype)) - lm_seq = torch.cat(parts) - lm_input_list.append(lm_seq) - lm_lengths.append(lm_seq.shape[0]) - - # Original protein-token position → LM input position. - prot_pos_b = mask_b.nonzero(as_tuple=True)[0] - expand_map = torch.full((L,), -1, device=device, dtype=torch.long) - expand_map[prot_pos_b] = per_token_lm_pos[inverse_ordered] - expand_maps.append(expand_map) - - # Pad to longest LM input, optionally rounding up to ``pad_to_multiple``. - max_len = max(lm_lengths) - if pad_to_multiple is not None and pad_to_multiple > 1: - max_len = ((max_len + pad_to_multiple - 1) // pad_to_multiple) * pad_to_multiple - lm_input_ids = torch.full( - (B, max_len), - 1, - device=device, - dtype=input_ids.dtype, # PAD=1 - ) - for b in range(B): - lm_input_ids[b, : lm_lengths[b]] = lm_input_list[b] - - # sequence_id for chain-aware attention; PAD tokens get -1 (no attention). - sequence_id = (lm_input_ids == 0).cumsum(dim=1) - 1 # BOS=0 - sequence_id = sequence_id.masked_fill(lm_input_ids == 1, -1) # PAD=1 - - with torch.inference_mode(): - esmc_out = esmc(input_ids=lm_input_ids, sequence_id=sequence_id, output_hidden_states=True) - - # ESMC returns hidden states as the standard tuple of per-layer tensors; stack - # them into the single [n_layers+1, B, max_len, D] tensor the projection expects. - hs = torch.stack(esmc_out.hidden_states, dim=0) # [n_layers+1, B, max_len, D] - n_layers_plus_1, _, _, D = hs.shape - result = torch.zeros(B, L, n_layers_plus_1, D, device=device, dtype=hs.dtype) - for b in range(B): - mb = protein_mask[b] - em = expand_maps[b][mb] # [n_protein_tokens] LM positions - # hs[:, b, em, :] -> [n_layers+1, n_protein_tokens, D] - gathered = hs[:, b, em, :].permute(1, 0, 2) - result[b, mb.nonzero(as_tuple=True)[0]] = gathered - - return result.detach() - - -# =========================================================================== -# ESMFold2TriangleMultiplicativeUpdate -# =========================================================================== -@use_kernel_forward_from_hub("ESMFold2TriangleMultiplication") -class ESMFold2TriangleMultiplicativeBlock(nn.Module): - """Triangle multiplicative update block with gated signal routing. - - The O(N^3) triangular contraction below is the trunk's dominant cost. Loading - with ``ESMFold2Model.from_pretrained(..., device_map="cuda", use_kernels=True)`` - (CUDA + inference) swaps the whole block forward for a fused Triton kernel from - the Hub (see the ``hub_kernels`` mapping); the pure-PyTorch ``forward`` here stays - as the reference/fallback. The kernel reads this module's parameters - (``norm_start``/``norm_mix``/``proj_bundle``/``proj_emit``/``proj_gate``) and - matches ``forward``'s ``(pair_grid, visibility)`` signature, returning the - residual-free delta. - """ - - _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} - _VALID_FLOWS = ("outgoing", "incoming") - - def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None: - super().__init__() - if flow not in self._FLOW_TO_EINSUM: - raise ValueError(f"Invalid flow={flow!r}. Expected one of {self._VALID_FLOWS}.") - - self.input_channels = input_channels - self.latent_channels = latent_channels - self.flow = flow - self._einsum_equation = self._FLOW_TO_EINSUM[flow] - self.norm_start = ESMFold2LayerNorm(self.input_channels, eps=_EPS) - self.norm_mix = ESMFold2LayerNorm(self.latent_channels, eps=_EPS) - self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) - self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) - self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) - - # Default chunked for memory on long sequences; tests override with - # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 - # parity checks. - self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def _triangular_contract(self, left_stream: Tensor, right_stream: Tensor) -> Tensor: - return torch.einsum(self._einsum_equation, left_stream, right_stream) - - def _triangular_contract_chunked(self, left_stream: Tensor, right_stream: Tensor, chunk_size: int) -> Tensor: - """Compute the triangular einsum in chunks along the output i-dimension.""" - L = left_stream.shape[1] if self.flow == "outgoing" else left_stream.shape[2] - chunks = [] - for start in range(0, L, chunk_size): - end = min(start + chunk_size, L) - if self.flow == "outgoing": - chunk = torch.einsum(self._einsum_equation, left_stream[:, start:end], right_stream) - else: - chunk = torch.einsum(self._einsum_equation, left_stream[:, :, start:end], right_stream) - chunks.append(chunk) - return torch.cat(chunks, dim=1) - - def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor: - if visibility is None: - visibility = pair_grid.new_ones(pair_grid.shape[:-1]) - - normalized_grid = self.norm_start(pair_grid) - bundled = self.proj_bundle(normalized_grid) - signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) - # Gates and the O(N^3) contraction run in the activation dtype (bf16). This - # matches the reference: under its autocast the einsum is downcast to bf16, - # and the fused Triton kernel likewise contracts in bf16 — the dtype the - # checkpoint was trained with. Keeping these in fp32 was a (marginal) - # precision *up* that diverges from training and is slower on the trunk's - # dominant op. ``norm_start``/``norm_mix`` stay fp32. A no-op in fp32. - routed = signal * torch.sigmoid(gate_logits) - routed = routed * visibility.unsqueeze(-1) - - left_stream, right_stream = routed.chunk(2, dim=-1) - if self._chunk_size is not None: - contracted = self._triangular_contract_chunked(left_stream, right_stream, self._chunk_size) - else: - contracted = self._triangular_contract(left_stream, right_stream) - mixed = self.proj_emit(self.norm_mix(contracted.float()).to(self.proj_emit.weight.dtype)) - output_gate = torch.sigmoid(self.proj_gate(normalized_grid)) - return mixed * output_gate - - -class ESMFold2TriangleMultiplicativeUpdate(nn.Module): - """Thin wrapper exposing the triangular mixer with explicit orientation (v3).""" - - def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: - super().__init__() - flow = "outgoing" if _outgoing else "incoming" - self._engine = ESMFold2TriangleMultiplicativeBlock(input_channels=dim, latent_channels=dim, flow=flow) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._engine.set_chunk_size(chunk_size) - - def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: - return self._engine(z, visibility=mask) - - -# =========================================================================== -# ESMFold2FoldingTrunk: ESMFold2Transition, ESMFold2PairUpdateBlock, ESMFold2FoldingTrunk -# =========================================================================== - - -class ESMFold2Transition(nn.Module): - """LayerNorm + ESMFold2SwiGLU feed-forward residual block, chunked along the token axis.""" - - def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: - super().__init__() - self.norm = ESMFold2LayerNorm(d_model) - self.ffn = ESMFold2SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) - # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. - self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def forward(self, x: Tensor) -> Tensor: - if self._chunk_size is None or x.shape[1] <= self._chunk_size: - return x + self.ffn(self.norm(x)) - out_list: list[Tensor] = [] - for s in range(0, x.shape[1], self._chunk_size): - e = min(s + self._chunk_size, x.shape[1]) - sl = x[:, s:e] - out_list.append(sl + self.ffn(self.norm(sl))) - return torch.cat(out_list, dim=1) - - -class ESMFold2PairUpdateBlock(nn.Module): - """tri_mul_out, tri_mul_in, pair_transition.""" - - def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: - super().__init__() - self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) - self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=expansion_ratio) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self.tri_mul_out.set_chunk_size(chunk_size) - self.tri_mul_in.set_chunk_size(chunk_size) - self.pair_transition.set_chunk_size(chunk_size) - - def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: - # HF model is inference-only, so the trained row-shared dropout (r=0) is a no-op. - pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) - pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) - pair = self.pair_transition(pair) - return pair - - -class ESMFold2FoldingTrunk(nn.Module): - """ModuleList of PairUpdateBlocks.""" - - def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4) -> None: - super().__init__() - self.blocks = nn.ModuleList( - [ESMFold2PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) for _ in range(n_layers)] - ) - - def set_chunk_size(self, chunk_size: int | None) -> None: - for block in self.blocks: - block.set_chunk_size(chunk_size) - - def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: - for block in self.blocks: - fn = partial(block, pair_attention_mask=pair_attention_mask) - if torch.is_grad_enabled(): - pair = checkpoint(fn, pair, use_reentrant=False) - else: - pair = fn(pair) - return pair - - -# =========================================================================== -# MSA Encoder -# =========================================================================== - - -class ESMFold2OuterProductMean(nn.Module): - """Outer-product mean: maps an MSA representation into a pair update. - - The order of the ``/ n_valid`` divide vs. the ``Wout`` projection is - selectable via ``divide_outer_before_proj`` because different ESMFold2 - checkpoints were trained with different orderings: - - * ``False`` (default): ``Wout(outer) / n_valid`` — the projection bias - is scaled by 1/n_valid alongside the outer product. - * ``True``: ``Wout(outer / n_valid)`` — the projection bias is added - unscaled, post-divide. - """ - - def __init__( - self, - d_msa: int, - d_hidden: int, - d_pair: int, - divide_outer_before_proj: bool = False, - ) -> None: - super().__init__() - self.d_hidden = d_hidden - self.divide_outer_before_proj = divide_outer_before_proj - self.norm = ESMFold2LayerNorm(d_msa) - self.W = nn.Linear(d_msa, 2 * d_hidden, bias=False) - self.Wout = nn.Linear(d_hidden * d_hidden, d_pair, bias=True) - # Off for bit-exact bf16; ``set_chunk_size(64)`` for long sequences. - self._chunk_size: int | None = None - - def set_chunk_size(self, chunk_size: int | None) -> None: - self._chunk_size = chunk_size - - def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: - m_norm = self.norm(m) - x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) - a, b = x.chunk(2, dim=-1) - mask_f = msa_attention_mask.to(a.dtype) - n_valid = (mask_f @ mask_f.transpose(-1, -2)).unsqueeze(-1).clamp(min=1.0) - if self._chunk_size is None: - outer = torch.einsum("bimc,bjmd->bijcd", a, b).flatten(-2) - if self.divide_outer_before_proj: - return self.Wout(outer / n_valid) - return self.Wout(outer) / n_valid - # Chunk along the left (i) axis so the peak einsum intermediate is - # [B, chunk, L, c, d] instead of [B, L, L, c, d]. - L = a.shape[1] - out_chunks: list[Tensor] = [] - for s in range(0, L, self._chunk_size): - e = min(s + self._chunk_size, L) - outer_chunk = torch.einsum("bimc,bjmd->bijcd", a[:, s:e], b).flatten(-2) - if self.divide_outer_before_proj: - out_chunks.append(self.Wout(outer_chunk / n_valid[:, s:e])) - else: - out_chunks.append(self.Wout(outer_chunk) / n_valid[:, s:e]) - return torch.cat(out_chunks, dim=1) - - -class ESMFold2MSAPairWeightedAveraging(nn.Module): - """Pair-biased MSA row update (AF3 Supplement Algorithm 10).""" - - def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = 32) -> None: - super().__init__() - self.n_heads = n_heads - self.head_width = head_width - self.norm_single = ESMFold2LayerNorm(d_msa) - self.compute_bias = nn.Sequential(ESMFold2LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) - self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) - self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) - self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) - - def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tensor) -> Tensor: - """ - Args: - msa_repr: [B, L, M, d_msa] - pair_repr: [B, L, L, d_pair] - pair_attention_mask:[B, L, L] - Returns: - [B, L, M, d_msa] - """ - B, L, M, _ = msa_repr.shape - h, dh = self.n_heads, self.head_width - - msa_normed = self.norm_single(msa_repr) - bias = self.compute_bias[1](self.compute_bias[0](pair_repr)) # [B, L, L, n_heads] - bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) - attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j - - v = self.Wv(msa_normed).reshape(B, L, M, h, dh) - gate = torch.sigmoid(self.Wgate(msa_normed)).reshape(B, L, M, h, dh) - - output = torch.einsum("bijh,bjmhd,bimhd->bimhd", attn, v, gate) - return self.Wout(output.reshape(B, L, M, h * dh)) - - -_CONFIDENCE_EPS = 1e-6 -_NONPOLYMER_ID = 4 - - -@dataclass -class ESMFold2Output(ModelOutput): - """ - Output of [`ESMFold2Model`]. All confidence scores are on a 0-1 scale; per-sample tensors - have a leading `num_diffusion_samples` axis. - - Args: - distogram_logits (`torch.FloatTensor` of shape `(batch_size, num_tokens, num_tokens, distogram_bins)`): - Predicted distance-distribution logits over residue pairs (RNG-independent; no diffusion sampling). - sample_atom_coords (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, 3)`): - Predicted all-atom Cartesian coordinates for each diffusion sample. - plddt_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, num_plddt_bins)`): - Per-atom pLDDT bin logits. - plddt (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens)`): - Per-residue predicted lDDT confidence. - plddt_per_atom (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms)`): - Per-atom predicted lDDT confidence. - plddt_ca (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens)`): - Predicted lDDT at the representative (Cα) atom of each token. - complex_plddt (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): - Mean pLDDT over all atoms of the complex. - complex_iplddt (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): - Interface-weighted complex pLDDT. - pae_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens, num_pae_bins)`): - Predicted-aligned-error bin logits. - pae (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens)`): - Expected predicted aligned error (Å) for each residue pair. - pde_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens, num_pde_bins)`): - Predicted-distance-error bin logits. - pde (`torch.FloatTensor` of shape `(num_diffusion_samples, num_tokens, num_tokens)`): - Expected predicted distance error (Å) for each residue pair. - resolved_logits (`torch.FloatTensor` of shape `(num_diffusion_samples, num_atoms, 2)`): - Per-atom resolved/unresolved logits. - ptm (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): - Predicted TM-score for each sample. - iptm (`torch.FloatTensor` of shape `(num_diffusion_samples,)`): - Predicted interface TM-score for each sample. - pair_chains_iptm (`torch.FloatTensor` of shape `(num_diffusion_samples, num_chains, num_chains)`): - Predicted interface TM-score for each ordered chain pair. - """ - - distogram_logits: Tensor | None = None - sample_atom_coords: Tensor | None = None - plddt_logits: Tensor | None = None - plddt: Tensor | None = None - plddt_per_atom: Tensor | None = None - plddt_ca: Tensor | None = None - complex_plddt: Tensor | None = None - complex_iplddt: Tensor | None = None - pae_logits: Tensor | None = None - pae: Tensor | None = None - pde_logits: Tensor | None = None - pde: Tensor | None = None - resolved_logits: Tensor | None = None - ptm: Tensor | None = None - iptm: Tensor | None = None - pair_chains_iptm: Tensor | None = None - - -class ESMFold2ConfidenceHead(nn.Module): - """Predicts pLDDT, PAE, PDE, resolved-atom probability and distogram bins.""" - - boundaries: Tensor - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__() - ch = config.confidence_head - d_single = config.d_single - d_pair = config.d_pair - d_inputs = config.inputs.d_inputs - - boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1) - self.register_buffer("boundaries", boundaries) - self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair) - - self.s_norm = ESMFold2LayerNorm(d_single) - self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False) - self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False) - self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False) - self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False) - self.s_inputs_norm = ESMFold2LayerNorm(d_inputs) - self.z_norm = ESMFold2LayerNorm(d_pair) - - self.row_attention_pooling = ESMFold2RowAttentionPooling(d_pair=d_pair, d_single=d_single) - - pf = ch.folding_trunk - self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) - - # Heads. - self.plddt_ln = ESMFold2LayerNorm(d_single) - max_atoms_per_token = 23 - self.plddt_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)) - - self.pae_ln = ESMFold2LayerNorm(d_pair) - self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False) - - self.pde_ln = ESMFold2LayerNorm(d_pair) - self.pde_head = nn.Linear(d_pair, ch.num_pde_bins, bias=False) - - self.resolved_ln = ESMFold2LayerNorm(d_single) - # 2 = resolved logits ([unresolved, resolved]). - self.resolved_weight = nn.Parameter(torch.zeros(max_atoms_per_token, d_single, 2)) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self.folding_trunk.set_chunk_size(chunk_size) - - @staticmethod - def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor: - return x if num_diffusion_samples == 1 else x.repeat_interleave(num_diffusion_samples, 0) - - @staticmethod - def _flatten_sample_axis(x: Tensor) -> Tensor: - if x.ndim == 4: - b, mult, n, c = x.shape - return x.reshape(b * mult, n, c) - return x - - def forward( - self, - s_inputs: Tensor, - z: Tensor, - x_pred: Tensor, - distogram_atom_idx: Tensor, - token_attention_mask: Tensor, - atom_to_token: Tensor, - atom_attention_mask: Tensor, - asym_id: Tensor, - mol_type: Tensor, - num_diffusion_samples: int = 1, - relative_position_encoding: Tensor | None = None, - token_bonds_encoding: Tensor | None = None, - ) -> dict[str, Tensor]: - s_inputs_normed = self.s_inputs_norm(s_inputs) - - z_base = self.z_norm(z) - if relative_position_encoding is not None: - z_base = z_base + relative_position_encoding - if token_bonds_encoding is not None: - z_base = z_base + token_bonds_encoding - z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2) - z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1) - z_base = z_base + self.s_to_z_prod_out( - self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :] * self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :] - ) - - pair = self._repeat_batch(z_base, num_diffusion_samples) - x_pred_flat = self._flatten_sample_axis(x_pred) - atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples) - atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples) - rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long() - mask = self._repeat_batch(token_attention_mask, num_diffusion_samples) - Bm = pair.shape[0] - - rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) - rep_distances = torch.cdist(rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist") - distogram_bins = (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() - pair = pair + self.dist_bin_pairwise_embed(distogram_bins) - - pair_mask = mask[:, :, None].float() * mask[:, None, :].float() - - # `pair` is fp32 here (built from the fp32 trunk output `z`); run the - # folding trunk in the model's compute dtype, then accumulate in fp32. - pair_delta = self.folding_trunk(pair.to(self.pae_head.weight.dtype), pair_attention_mask=pair_mask) - pair.add_(pair_delta.float()) - del pair_delta - # Accumulated in fp32; hand the downstream confidence heads the compute dtype. - pair = pair.to(self.pae_head.weight.dtype) - single = self.row_attention_pooling(pair, mask) - - atom_mask_f = atom_mask_m.float() - s_at_atoms = gather_token_to_atom(single, atom_to_token_m) - s_at_atoms_ln = self.plddt_ln(s_at_atoms) - - intra_idx = _compute_intra_token_idx(atom_to_token_m) - intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1) - w_plddt = self.plddt_weight[intra_idx] - plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms_ln, w_plddt) - plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0) - - L = single.shape[1] - plddt_sum = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) - atom_count = torch.zeros(Bm, L, device=single.device, dtype=plddt_per_atom.dtype) - atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype) - plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t) - atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t) - plddt = plddt_sum / atom_count.clamp(min=1e-6) - - complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (atom_mask_f.sum(dim=-1) + _CONFIDENCE_EPS) - - expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) - expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) - is_ligand = (expanded_type == _NONPOLYMER_ID).float() - inter_chain = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() - near_contact = (rep_distances < 8).float() - interface_per_token = (near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1)).amax(dim=-1) - iplddt_weight = torch.where( - is_ligand.bool(), - torch.full_like(interface_per_token, 2.0), - interface_per_token, - ) - iplddt_weight_atoms = gather_token_to_atom(iplddt_weight.unsqueeze(-1), atom_to_token_m).squeeze(-1) - atom_iplddt_w = atom_mask_f * iplddt_weight_atoms - complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (atom_iplddt_w.sum(dim=-1) + _CONFIDENCE_EPS) - - plddt_ca = plddt_per_atom.gather(1, rep_idx_m) - - # PAE - pae_logits = self.pae_head(self.pae_ln(pair)) - pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() - - # PDE - pde_logits = self.pde_head(self.pde_ln(pair)) - pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() - - # Resolved (per-atom binary). - s_at_atoms_res = self.resolved_ln(s_at_atoms) - w_res = self.resolved_weight[intra_idx] - resolved_logits = torch.einsum("...c,...cb->...b", s_at_atoms_res, w_res) - - # pTM / ipTM from pae_logits. - n_bins = pae_logits.shape[-1] - bin_width = 32.0 / n_bins - bin_centers = torch.arange(0.5 * bin_width, 32.0, bin_width, device=pae_logits.device) - mask_f = mask.float() - N_res = mask_f.sum(dim=-1, keepdim=True) - d0 = 1.24 * (N_res.clamp(min=19) - 15) ** (1 / 3) - 1.8 - tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2) - pae_probs = F.softmax(pae_logits, dim=-1, dtype=torch.float32) - tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1) - - pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2) - ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (pair_mask_2d.sum(dim=-1) + _CONFIDENCE_EPS) - ptm = ptm_per_row.max(dim=-1).values - - inter_chain_mask = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() * pair_mask_2d - iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (inter_chain_mask.sum(dim=-1) + _CONFIDENCE_EPS) - iptm = iptm_per_row.max(dim=-1).values - - max_chain_id = int(expanded_asym.max().item()) if Bm > 0 else 0 - n_chains = max_chain_id + 1 - pair_chains_iptm = torch.zeros(Bm, n_chains, n_chains, device=tm_expected.device, dtype=tm_expected.dtype) - for c1 in range(n_chains): - chain_c1 = (expanded_asym == c1).float() * mask_f - if chain_c1.sum() == 0: - continue - for c2 in range(n_chains): - chain_c2 = (expanded_asym == c2).float() * mask_f - pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2) - denom = pair_m.sum(dim=(-1, -2)) + _CONFIDENCE_EPS - pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(dim=(-1, -2)) / denom - - return { - "plddt_logits": plddt_logits, - "plddt": plddt.detach(), - "plddt_per_atom": plddt_per_atom.detach(), - "plddt_ca": plddt_ca.detach(), - "complex_plddt": complex_plddt.detach(), - "complex_iplddt": complex_iplddt.detach(), - "pae_logits": pae_logits, - "pae": pae, - "pde_logits": pde_logits, - "pde": pde, - "resolved_logits": resolved_logits, - "ptm": ptm.detach(), - "iptm": iptm.detach(), - "pair_chains_iptm": pair_chains_iptm.detach(), - } - - -def _inverse_softplus(value: float) -> float: - return value + math.log(-math.expm1(-value)) - - -class ESMFold2PreTrainedModel(PreTrainedModel): - """Base class for ESMFold2 — declares the loading and weight-initialization behaviour.""" - - config_class = ESMFold2Config - base_model_prefix = "esmfold2" - main_input_name = "token_index" - _no_split_modules = [ - "ESMCLayer", - "ESMFold2PairUpdateBlock", - "ESMFold2AtomEncoder", - "ESMFold2AtomDecoder", - "ESMFold2DiffusionTransformer", - ] - _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] - # The Fourier noise-embedding frequencies/phases are random Gaussian features whose - # precision drives the diffusion conditioning; keep them fp32 even under dtype=bf16. - _keep_in_fp32_modules_strict = ["fourier"] - _supports_sdpa = True - _supports_flash_attn = True - _supports_attention_backend = True - - def _init_weights(self, module): - # The base initializer handles Linear/Embedding/LayerNorm; below we (re)apply the - # few non-default inits the architecture needs (adaLN-Zero gates, identity/recurrent - # parcae params, zeroed projections). These live here, not in submodule __init__s, - # so they survive `post_init()` and are not wastefully run before weight loading. - # The `init.*` helpers are load-flag aware (they no-op on already-loaded weights). - super()._init_weights(module) - if isinstance(module, ESMFold2Model): - init.eye_(module.parcae_readout.weight) - init.eye_(module.parcae_b_cont) - init.zeros_(module.parcae_log_a) - parcae_delta_init = -math.log(math.sqrt(1.0 / 5.0)) - init.constant_(module.parcae_log_delta, _inverse_softplus(parcae_delta_init)) - elif isinstance(module, ESMFold2ConfidenceHead): - init.zeros_(module.plddt_weight) - init.zeros_(module.resolved_weight) - elif isinstance(module, ESMFold2AdaptiveLayerNorm): - init.ones_(module.s_scale) - elif isinstance(module, ESMFold2SWAAtomBlock): - init.zeros_(module.adaln_modulation[1].weight) - elif isinstance(module, ESMFold2AttentionPairBias): - if getattr(module, "out_gate", None) is not None: - init.zeros_(module.out_gate.weight) - init.constant_(module.out_gate.bias, -2.0) - elif isinstance(module, ESMFold2ConditionedTransitionBlock): - if getattr(module, "output_gate", None) is not None: - init.zeros_(module.output_gate.weight) - init.constant_(module.output_gate.bias, -2.0) - elif isinstance(module, ESMFold2DiffusionModule): - init.zeros_(module.s_to_token.weight) - elif isinstance(module, ESMFold2LanguageModelShim): - init.zeros_(module.base_z_combine) - - -class ESMFold2Model(ESMFold2PreTrainedModel): - """ESMFold2 — all-atom structure prediction with an ESMC PLM backbone. - - This is the standard released ESMFold2 architecture (uses a linear- - recurrent trunk, internally referred to as "parcae"). - - Forward kwargs that callers commonly override: - - * ``num_loops`` (default ``config.num_loops``): trunk refinement - loops. - * ``num_diffusion_samples`` (default ``config.num_diffusion_samples``): - parallel structure samples; the confidence head re-runs once per - sample, so memory scales linearly. Pass ``1`` for cheap inference. - * ``num_sampling_steps`` (default ``config.structure_head.inference_num_steps``): - diffusion ODE solver steps. Lower for speed, higher for quality. - - Memory / perf knobs: - - * ``model.set_chunk_size(int|None)``: caps L² ops (triangle / OPM / - pair transition) at this token-axis chunk. Default 64 — fits - L≈2k on an 80 GB GPU. Pass ``None`` for faster inference at L<600. - """ - - def __init__(self, config: ESMFold2Config) -> None: - super().__init__(config) - d_inputs = config.inputs.d_inputs - d_pair = config.d_pair - - self.inputs_embedder = ESMFold2InputsEmbedder(config) - self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) - self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False) - self.rel_pos = ESMFold2ResIdxAsymIdSymIdEntityIdEncoding( - n_relative_residx_bins=config.n_relative_residx_bins, - n_relative_chain_bins=config.n_relative_chain_bins, - d_pair=d_pair, - ) - self.token_bonds = nn.Linear(1, d_pair, bias=False) - self.language_model = ESMFold2LanguageModelShim( - d_z=d_pair, d_model=config.esmc_config.d_model, num_layers=config.esmc_config.n_layers - ) - # The ESMC backbone is a bundled submodule: built here with random weights - # (instant, no I/O), then populated by the parent's from_pretrained like any - # other composite sub-model (its weights live under ``esmc.*`` in the ESMFold2 - # checkpoint). It is frozen in effect — ``forward`` detaches its hidden states - # before they enter the trunk, so no gradient ever reaches it — and the bf16 - # autocast in ``_compute_lm_hidden_states`` reproduces the LM precision regime. - self.esmc = AutoModel.from_config(config.esmc_config) - - pf = config.folding_trunk - self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) - if config.lm_encoder.enabled: - self.lm_encoder: ESMFold2FoldingTrunk | None = ESMFold2FoldingTrunk( - n_layers=config.lm_encoder.n_layers, d_pair=d_pair, expansion_ratio=4 - ) - else: - self.lm_encoder = None - - # parcae linear-recurrence params (allocated here; initialized in _init_weights): - # log_a -> 0, log_delta -> a fixed decay constant, b_cont -> identity, readout -> identity. - self.parcae_input_norm = ESMFold2LayerNorm(d_pair) - self.parcae_log_a = nn.Parameter(torch.zeros(d_pair)) - self.parcae_log_delta = nn.Parameter(torch.empty(d_pair, dtype=torch.float32)) - self.parcae_b_cont = nn.Parameter(torch.empty(d_pair, d_pair)) - self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) - self.parcae_coda = ESMFold2FoldingTrunk(n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4) - - # Heads -------------------------------------------------------------- - self.structure_head = ESMFold2DiffusionStructureHead(config) - self.distogram_head = nn.Linear(d_pair, config.structure_head.distogram_bins, bias=True) - self.confidence_head = ESMFold2ConfidenceHead(config) - - msa_cfg = config.msa_encoder - self.msa_encoder = None - if msa_cfg.enabled: - self.msa_encoder = ESMFold2MSAEncoder( - d_msa=msa_cfg.d_msa, - d_pair=d_pair, - d_inputs=d_inputs, - d_hidden=msa_cfg.d_hidden, - n_layers=msa_cfg.n_layers, - n_heads_msa=msa_cfg.n_heads_msa, - msa_head_width=msa_cfg.msa_head_width, - ) - - # ESMFold2SWA3DRoPEAttention modules live deep in the atom encoders/decoders and - # are built from explicit dims, so give each a handle to the model config: - # their forward dispatches the plain-attention core through the v5 - # attention interface (config._attn_implementation), staying live under - # set_attn_implementation() since the config object is shared. - for module in self.modules(): - if isinstance(module, ESMFold2SWA3DRoPEAttention): - module.config = self.config - - self.post_init() - - def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = None) -> None: - """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" - import torch._dynamo - - torch._dynamo.config.cache_size_limit = 512 - torch._dynamo.config.accumulated_cache_size_limit = 512 - # capture_scalar_outputs avoids graph breaks at .item() in atom-attention path. - torch._dynamo.config.capture_scalar_outputs = True - - if dynamic is None: - dynamic = mode == "dynamic_seqlen" - kwargs: dict = {"dynamic": dynamic} - - compile_targets = ( - ESMFold2PairUpdateBlock, - ESMFold2DiffusionTransformer, - ESMFold2DiffusionModule, - ESMFold2MSAEncoderBlock, - ) - - def _maybe_compile(module: nn.Module) -> None: - if isinstance(module, compile_targets): - module.forward = torch.compile(module.forward, **kwargs) - - self.apply(_maybe_compile) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self.folding_trunk.set_chunk_size(chunk_size) - if self.lm_encoder is not None: - self.lm_encoder.set_chunk_size(chunk_size) - self.parcae_coda.set_chunk_size(chunk_size) - self.confidence_head.set_chunk_size(chunk_size) - if self.msa_encoder is not None: - self.msa_encoder.set_chunk_size(chunk_size) - - def _compute_lm_hidden_states( - self, - input_ids: Tensor, - asym_id: Tensor, - residue_index: Tensor, - mol_type: Tensor, - tok_mask: Tensor, - ) -> Tensor: - # Run the frozen ESMC backbone exactly as the reference does: plain weights - # under a bf16 autocast (the fork's ``_lm_precision_context``). For a bf16 - # backbone this puts norms/softmax in fp32 and matmuls/rotary in bf16, - # reproducing the checkpoint's training regime bit-for-bit; disabled (a - # no-op) for an fp32 backbone. This autocast is scoped to the backbone only - # — the trunk stays dtype-honest. - use_amp = next(self.esmc.parameters()).dtype == torch.bfloat16 - with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=use_amp): - return compute_lm_hidden_states( - self.esmc, - input_ids, - asym_id, - residue_index, - mol_type, - tok_mask, - pad_to_multiple=None, - ) - - def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: - delta = F.softplus(self.parcae_log_delta) - a = torch.exp(-delta * torch.exp(self.parcae_log_a)) - b = delta[:, None] * self.parcae_b_cont - return a, b - - def _init_pair_state(self, ref: Tensor) -> Tensor: - std = math.sqrt(2.0 / (5.0 * ref.shape[-1])) - state = torch.empty_like(ref, dtype=torch.float32) - nn.init.trunc_normal_(state, mean=0.0, std=std, a=-3 * std, b=3 * std) - return state.to(dtype=ref.dtype) - - def _run_one_loop( - self, - z: Tensor, - z_init: Tensor, - lm_z: Tensor | None, - _msa_kwargs: dict | None, - pair_mask: Tensor, - a: Tensor, - b_mat: Tensor, - total_steps: int, - ) -> Tensor: - # Helper method (not inline) so per-iter locals free on return — - # otherwise leaks ~2 GB L²×c_z into distogram/sample scope. - # training=True forces dropout under eval(), matching the per-loop - # dropout strategy used at train time. - lm_cfg = self.config.lm_encoder - _per_loop_lm_dropout = ( - lm_z is not None - and getattr(lm_cfg, "per_loop_lm_dropout", False) - and getattr(lm_cfg, "lm_dropout", 0.0) > 0.0 - ) - _lm_dropout_p = getattr(lm_cfg, "lm_dropout", 0.0) - - for _ in range(total_steps): - if _per_loop_lm_dropout: - assert lm_z is not None # narrowed by _per_loop_lm_dropout - lm_z_i: Tensor | None = F.dropout(lm_z, p=_lm_dropout_p, training=True) - else: - lm_z_i = lm_z - - refined_lm_z: Tensor | None = None - if lm_z_i is not None and self.lm_encoder is not None: - refined_lm_z = self.lm_encoder(lm_z_i.to(z_init.dtype), pair_attention_mask=pair_mask) - - z_inject_pair = z_init - if lm_z_i is not None and self.lm_encoder is None: - z_inject_pair = z_inject_pair + lm_z_i.to(z_inject_pair.dtype) - - if self.msa_encoder is not None and _msa_kwargs is not None: - msa_pair = self.msa_encoder(x_pair=z_inject_pair, **_msa_kwargs).to(z_inject_pair.dtype) - z_inject_pair = msa_pair if self.config.msa_encoder_overwrite else (z_inject_pair + msa_pair) - - if refined_lm_z is not None: - z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) - - injected_pair = self.parcae_input_norm(z_inject_pair) - z = a * z + F.linear(injected_pair.to(z.dtype), b_mat) - z = self.folding_trunk(z, pair_attention_mask=pair_mask) - - return z - - def forward( - self, - token_index: Tensor, - residue_index: Tensor, - asym_id: Tensor, - sym_id: Tensor, - entity_id: Tensor, - mol_type: Tensor, - res_type: Tensor, - token_bonds: Tensor, - token_attention_mask: Tensor, - ref_pos: Tensor, - ref_element: Tensor, - ref_charge: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - atom_attention_mask: Tensor, - atom_to_token: Tensor, - distogram_atom_idx: Tensor, - deletion_mean: Tensor | None = None, - msa: Tensor | None = None, - has_deletion: Tensor | None = None, - deletion_value: Tensor | None = None, - msa_attention_mask: Tensor | None = None, - input_ids: Tensor | None = None, - lm_hidden_states: Tensor | None = None, - num_loops: int | None = None, - num_diffusion_samples: int | None = None, - num_sampling_steps: int | None = None, - **kwargs, - ) -> ESMFold2Output: - tok_mask = token_attention_mask - atm_mask = atom_attention_mask - disto_idx = distogram_atom_idx - - n_loops: int = num_loops if num_loops is not None else self.config.num_loops - n_samples: int = ( - num_diffusion_samples if num_diffusion_samples is not None else self.config.num_diffusion_samples - ) - total_steps = max(1, n_loops + 1) - - if res_type.dim() == 2: - res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float() - res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() - else: - res_type_oh = res_type.float() - - if msa is not None: - msa_oh_profile = F.one_hot(msa.long(), num_classes=NUM_RES_TYPES).float() - if msa_attention_mask is not None: - mask_f = msa_attention_mask.float().unsqueeze(-1) - msa_oh_profile = msa_oh_profile * mask_f - valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) - profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) - else: - profile = msa_oh_profile.mean(dim=1) - else: - profile = res_type_oh - - if deletion_mean is None: - deletion_mean = torch.zeros(res_type.shape[0], res_type.shape[1], device=res_type.device) - - ref_element_oh = F.one_hot(ref_element.long(), num_classes=MAX_ATOMIC_NUMBER).float() - ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE).float() - # Bias-free downstream Linears require zeroed padding. - atm_mask_f = atm_mask.float() - ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) - ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(-1).unsqueeze(-1) - atom_to_token = atom_to_token * atm_mask.long() - - x_inputs = self.inputs_embedder( - aatype=res_type_oh, - profile=profile.float(), - deletion_mean=deletion_mean.float(), - ref_pos=ref_pos, - atom_attention_mask=atm_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element_oh, - ref_atom_name_chars=ref_atom_name_chars_oh, - atom_to_token=atom_to_token, - ) - - z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(x_inputs).unsqueeze(1) - - relative_position_encoding = self.rel_pos( - residue_index=residue_index, - asym_id=asym_id, - sym_id=sym_id, - entity_id=entity_id, - token_index=token_index, - ) - token_bonds_encoding = self.token_bonds(token_bonds.to(self.token_bonds.weight.dtype)) - z_init = z_init + relative_position_encoding + token_bonds_encoding - - if lm_hidden_states is None and input_ids is not None: - lm_hidden_states = self._compute_lm_hidden_states(input_ids, asym_id, residue_index, mol_type, tok_mask) - lm_z: Tensor | None = None - if lm_hidden_states is not None: - lm_z = self.language_model(lm_hidden_states.detach()) - del lm_hidden_states - - pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float() - - z = self._init_pair_state(z_init) - - a, b = self._discretized_dynamics() - a = a.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) - b_mat = b.to(device=z.device, dtype=z.dtype) - - _msa_kwargs: dict | None = None - if self.msa_encoder is not None and msa is not None: - B_msa, M, L_msa = msa.shape - msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES).float() - msa_attn = ( - msa_attention_mask.permute(0, 2, 1).float() - if msa_attention_mask is not None - else tok_mask[:, :, None].expand(-1, -1, M).float() - ) - # Bias-free ESMFold2MSAEncoder.embed requires zeroed padding. - msa_oh = msa_oh * msa_attn.unsqueeze(-1) - hd = ( - has_deletion.permute(0, 2, 1).float() - if has_deletion is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - dv = ( - deletion_value.permute(0, 2, 1).float() - if deletion_value is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - _msa_kwargs = { - "x_inputs": x_inputs, - "msa_oh": msa_oh, - "has_deletion": hd, - "deletion_value": dv, - "msa_attention_mask": msa_attn, - } - - # Method call (not inline loop) frees per-iter L²×c_z locals. - z = self._run_one_loop( - z=z, - z_init=z_init, - lm_z=lm_z, - _msa_kwargs=_msa_kwargs, - pair_mask=pair_mask, - a=a, - b_mat=b_mat, - total_steps=total_steps, - ) - del z_init, lm_z, _msa_kwargs, a, b_mat - - z = self.parcae_readout(z) - z = self.parcae_coda(z, pair_attention_mask=pair_mask) - - z = z.float() - distogram_logits = self.distogram_head((z + z.transpose(-2, -3)).to(self.distogram_head.weight.dtype)) - - structure_output = self.structure_head.sample( - z_trunk=z, - s_inputs=x_inputs, - relative_position_encoding=relative_position_encoding, - ref_pos=ref_pos, - ref_charge=ref_charge, - ref_mask=atm_mask, - ref_element=ref_element_oh, - ref_atom_name_chars=ref_atom_name_chars_oh, - ref_space_uid=ref_space_uid, - tok_idx=atom_to_token, - asym_id=asym_id, - residue_index=residue_index, - entity_id=entity_id, - token_index=token_index, - sym_id=sym_id, - token_attention_mask=tok_mask, - num_diffusion_samples=n_samples, - num_sampling_steps=num_sampling_steps, - return_atom_repr=False, - denoising_early_exit_rmsd=None, - ) - - sample_coords = structure_output["sample_atom_coords"] - if sample_coords is None: - raise RuntimeError("The diffusion structure head did not return sampled coordinates.") - - confidence_output = self.confidence_head( - s_inputs=x_inputs.detach(), - z=z.detach().float(), - x_pred=sample_coords.detach(), - distogram_atom_idx=disto_idx, - token_attention_mask=tok_mask, - atom_to_token=atom_to_token, - atom_attention_mask=atm_mask, - asym_id=asym_id, - mol_type=mol_type, - num_diffusion_samples=n_samples, - relative_position_encoding=relative_position_encoding.detach(), - token_bonds_encoding=token_bonds_encoding.detach(), - ) - - return ESMFold2Output( - distogram_logits=distogram_logits, - sample_atom_coords=sample_coords, - **confidence_output, - ) - - @torch.no_grad() - def infer_protein(self, seq: str, **forward_kwargs) -> ESMFold2Output: - from .protein_utils import prepare_protein_features - - features = prepare_protein_features(seq) - features = {k: v.to(self.device) for k, v in features.items()} - return self(**features, **forward_kwargs) - - @torch.no_grad() - def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str: - from .protein_utils import output_to_pdb, prepare_protein_features - - features = prepare_protein_features(seq) - features = {k: v.to(self.device) for k, v in features.items()} - output = self(**features, **forward_kwargs) - return output_to_pdb(output, features) - - @staticmethod - def output_to_pdb(output: ESMFold2Output, features: dict[str, Tensor]) -> str: - """Render a PDB string from an [`ESMFold2Output`] and the input ``features`` it was produced from.""" - from .protein_utils import output_to_pdb as _output_to_pdb - - return _output_to_pdb(output, features) - - -class ESMFold2MSAEncoderBlock(nn.Module): - """One MSA encoder block: OPM into pair, MSA pair-weighted averaging, triangle update.""" - - def __init__( - self, - d_msa: int, - d_pair: int, - d_hidden: int, - n_heads_msa: int, - msa_head_width: int, - is_final_block: bool = False, - ) -> None: - super().__init__() - self.is_final_block = is_final_block - self.outer_product_mean = ESMFold2OuterProductMean(d_msa, d_hidden, d_pair) - if not is_final_block: - self.msa_pair_weighted_averaging = ESMFold2MSAPairWeightedAveraging( - d_msa, d_pair, n_heads_msa, msa_head_width - ) - self.msa_transition = ESMFold2Transition(d_msa, expansion_ratio=4) - self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) - self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=4) - - def set_chunk_size(self, chunk_size: int | None) -> None: - self.outer_product_mean.set_chunk_size(chunk_size) - self.tri_mul_out.set_chunk_size(chunk_size) - self.tri_mul_in.set_chunk_size(chunk_size) - if not self.is_final_block: - self.msa_transition.set_chunk_size(chunk_size) - self.pair_transition.set_chunk_size(chunk_size) - - def forward( - self, - m: Tensor, - pair: Tensor, - msa_attention_mask: Tensor, - pair_attention_mask: Tensor, - ) -> tuple[Tensor, Tensor]: - pair = pair + self.outer_product_mean(m, msa_attention_mask) - if not self.is_final_block: - m = m + self.msa_pair_weighted_averaging(m, pair, pair_attention_mask) - m = self.msa_transition(m) - pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) - pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) - pair = self.pair_transition(pair) - return m, pair - - -class ESMFold2MSAEncoder(nn.Module): - """Stack of [`ESMFold2MSAEncoderBlock`] layers that conditions the pair on an MSA.""" - - def __init__( - self, - d_msa: int, - d_pair: int, - d_inputs: int, - d_hidden: int = 32, - n_layers: int = 4, - n_heads_msa: int = 8, - msa_head_width: int = 16, - ) -> None: - super().__init__() - self.embed = nn.Linear(35, d_msa, bias=False) - self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) - self.blocks = nn.ModuleList( - [ - ESMFold2MSAEncoderBlock( - d_msa=d_msa, - d_pair=d_pair, - d_hidden=d_hidden, - n_heads_msa=n_heads_msa, - msa_head_width=msa_head_width, - is_final_block=(i == n_layers - 1), - ) - for i in range(n_layers) - ] - ) - - def set_chunk_size(self, chunk_size: int | None) -> None: - for block in self.blocks: - block.set_chunk_size(chunk_size) - - def forward( - self, - x_pair: Tensor, - x_inputs: Tensor, - msa_oh: Tensor, - has_deletion: Tensor, - deletion_value: Tensor, - msa_attention_mask: Tensor, - ) -> Tensor: - # All inputs are pre-transposed to [B, L, M, ...] before calling. - m_feat = torch.cat([msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1) - m = self.embed(m_feat.to(self.embed.weight.dtype)) + self.project_inputs(x_inputs).unsqueeze(2) - tok_mask = msa_attention_mask[:, :, 0].bool() - pair_attention_mask = tok_mask.unsqueeze(2) & tok_mask.unsqueeze(1) - for block in self.blocks: - m, x_pair = block(m, x_pair, msa_attention_mask, pair_attention_mask) - return x_pair - - -__all__ = ["ESMFold2Model", "ESMFold2PreTrainedModel"] From 783755962d774eceac122c4f30ba94bb628ddb0d Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Jun 2026 16:24:20 +0100 Subject: [PATCH 46/70] Dead code cleanup --- .../models/esmfold2/modeling_esmfold2.py | 157 ++++-------------- .../models/esmfold2/protein_utils.py | 1 - 2 files changed, 33 insertions(+), 125 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 4899d1048efc..589d3eec6fa4 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -524,15 +524,9 @@ def forward( q_l: Tensor, c_l: Tensor, attention_params: tuple, - return_intermediates: bool = False, - ) -> Tensor | tuple[Tensor, list[Tensor]]: - intermediates: list[Tensor] = [] + ) -> Tensor: for block in self.blocks: q_l = block(q_l, c_l, attention_params) - if return_intermediates: - intermediates.append(q_l) - if return_intermediates: - return q_l, intermediates return q_l @@ -647,10 +641,9 @@ def forward( r_l: Tensor | None = None, pred_r1: Tensor | None = None, num_diffusion_samples: int = 1, - return_intermediates: bool = False, inference_cache: dict | None = None, - ) -> tuple[Tensor, Tensor, Tensor, tuple, list[Tensor]]: - """Returns (a, q, c, attention_params, intermediates). + ) -> tuple[Tensor, Tensor, Tensor, tuple]: + """Returns (a, q, c, attention_params). ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, attention indices, n_tokens) across diffusion steps. @@ -711,17 +704,11 @@ def forward( c = c.repeat_interleave(num_diffusion_samples, 0) - result = self.atom_transformer( + q = self.atom_transformer( q_l=q, c_l=c, attention_params=attention_params, - return_intermediates=return_intermediates, ) - if return_intermediates: - q, intermediates = result - else: - q = result - intermediates = [] q_to_a = F.relu(self.atom_to_token_linear(q)) if layer_cache is not None and "atom_to_token_exp" in layer_cache: @@ -730,7 +717,7 @@ def forward( atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) - return a, q, c, attention_params, intermediates + return a, q, c, attention_params # =========================================================================== @@ -800,28 +787,21 @@ def forward( atom_to_token: Tensor, atom_attention_mask: Tensor, num_diffusion_samples: int = 1, - return_intermediates: bool = False, - ) -> tuple[Tensor, list[Tensor]]: - """Returns (r_update, intermediates).""" + ) -> Tensor: + """Returns r_update.""" atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) a_to_q = self.token_to_atom_linear(a_i) a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) q_l = q_l + a_to_q - result = self.atom_transformer( + q_l = self.atom_transformer( q_l=q_l, c_l=c_l, attention_params=p_lm, - return_intermediates=return_intermediates, ) - if return_intermediates: - q_l, intermediates = result - else: - q_l = result - intermediates = [] r_l = self.output_linear(self.norm(q_l)) - return r_l, intermediates + return r_l # =========================================================================== @@ -838,7 +818,6 @@ def __init__( d_pair: int, num_heads: int, d_cond: int | None = None, - use_conditioning: bool = True, ) -> None: super().__init__() self.d_model = d_model @@ -847,12 +826,9 @@ def __init__( self.scale = self.head_dim**-0.5 d_cond = d_cond or d_model - if use_conditioning: - self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) - # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. - self.out_gate = nn.Linear(d_cond, d_model, bias=True) - else: - self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. + self.out_gate = nn.Linear(d_cond, d_model, bias=True) self.q_proj = nn.Linear(d_model, d_model, bias=True) self.kv_proj = nn.Linear(d_model, 2 * d_model, bias=False) @@ -880,7 +856,7 @@ def compute_pair_bias(self, z: Tensor, bsz: int, num_diffusion_samples: int = 1) def forward( self, a: Tensor, - s: Tensor | None, + s: Tensor, z: Tensor, attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, @@ -888,10 +864,7 @@ def forward( ) -> Tensor: bsz, n_queries, d_model = a.shape - if s is not None: - x = self.adaln(a, s) - else: - x = self.pre_norm(a) + x = self.adaln(a, s) n_keys = x.shape[1] q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) @@ -924,8 +897,7 @@ def forward( ctx = g * ctx out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) - if s is not None: - out = torch.sigmoid(self.out_gate(s)) * out + out = torch.sigmoid(self.out_gate(s)) * out return out @@ -942,34 +914,26 @@ def __init__( d_model: int, d_cond: int | None = None, transition_multiplier: int = 2, - use_conditioning: bool = True, ) -> None: super().__init__() d_cond = d_cond or d_model hidden = transition_multiplier * d_model - if use_conditioning: - self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) - # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. - self.output_gate = nn.Linear(d_cond, d_model, bias=True) - else: - self.pre_norm = ESMFold2LayerNorm(d_model, eps=1e-5) + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. + self.output_gate = nn.Linear(d_cond, d_model, bias=True) self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) self.lin_out = nn.Linear(hidden, d_model, bias=False) - def forward(self, a: Tensor, s: Tensor | None) -> Tensor: - if s is not None: - x = self.adaln(a, s) - else: - x = self.pre_norm(a) + def forward(self, a: Tensor, s: Tensor) -> Tensor: + x = self.adaln(a, s) swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) b = F.silu(swish_a) * swish_b out = self.lin_out(b) - if s is not None: - out = torch.sigmoid(self.output_gate(s)) * out + out = torch.sigmoid(self.output_gate(s)) * out return out @@ -989,7 +953,6 @@ def __init__( num_blocks: int, d_cond: int | None = None, transition_multiplier: int = 2, - use_conditioning: bool = True, ) -> None: super().__init__() d_cond = d_cond or d_model @@ -1001,7 +964,6 @@ def __init__( d_pair=d_pair, num_heads=num_heads, d_cond=d_cond, - use_conditioning=use_conditioning, ) for _ in range(num_blocks) ] @@ -1012,7 +974,6 @@ def __init__( d_model=d_model, d_cond=d_cond, transition_multiplier=transition_multiplier, - use_conditioning=use_conditioning, ) for _ in range(num_blocks) ] @@ -1021,14 +982,12 @@ def __init__( def forward( self, a: Tensor, - s: Tensor | None, + s: Tensor, z: Tensor, attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, - return_intermediates: bool = False, inference_cache: dict | None = None, - ) -> tuple[Tensor, list[Tensor]]: - intermediates: list[Tensor] = [] + ) -> Tensor: x = a bsz = a.shape[0] # Each block's pair bias depends only on the (step-invariant) conditioning @@ -1053,9 +1012,7 @@ def forward( pair_bias=pair_bias, ) x = x + transition(x, s) - if return_intermediates: - intermediates.append(x) - return x, intermediates + return x # =========================================================================== @@ -1228,7 +1185,6 @@ def __init__( num_blocks=token_num_blocks, d_cond=c_token, transition_multiplier=transition_multiplier, - use_conditioning=True, ) self.s_step_norm = ESMFold2LayerNorm(c_token) @@ -1256,9 +1212,8 @@ def forward( sigma_data: float | None = None, token_attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, - return_atom_repr: bool = False, inference_cache: dict[str, Tensor] | None = None, - ) -> dict[str, Tensor | None]: + ) -> Tensor: bsz = x_noisy.shape[0] sigma = self.sigma_data if sigma_data is None else float(sigma_data) t = torch.as_tensor(t_hat, dtype=torch.float32, device=x_noisy.device).reshape(-1) @@ -1281,7 +1236,7 @@ def forward( r_noisy = x_noisy / denom[:, None, None] # Step 3: atom encoder - a, q_skip, c_skip, p_skip, enc_intermediates = self.atom_encoder( + a, q_skip, c_skip, p_skip = self.atom_encoder( ref_pos=ref_pos, atom_attention_mask=ref_mask, ref_space_uid=ref_space_uid, @@ -1291,7 +1246,6 @@ def forward( atom_to_token=tok_idx, r_l=r_noisy, num_diffusion_samples=num_diffusion_samples, - return_intermediates=return_atom_repr, inference_cache=inference_cache, ) @@ -1299,7 +1253,7 @@ def forward( a = a + self.s_to_token(self.s_step_norm(s)) # Step 5: token transformer (pair bias is cached across steps via inference_cache) - a, _ = self.token_transformer( + a = self.token_transformer( a, s, z, @@ -1312,7 +1266,7 @@ def forward( a = self.token_norm(a) # Step 7: atom decoder - r_update, dec_intermediates = self.atom_decoder( + r_update = self.atom_decoder( a_i=a, q_l=q_skip, c_l=c_skip, @@ -1320,7 +1274,6 @@ def forward( atom_to_token=tok_idx, atom_attention_mask=ref_mask, num_diffusion_samples=num_diffusion_samples, - return_intermediates=return_atom_repr, ) # Step 8: compute denoised output @@ -1329,17 +1282,7 @@ def forward( out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update - # Collect atom intermediates from encoder + decoder - atom_intermediates: Tensor | None = None - if return_atom_repr: - all_ints = enc_intermediates + dec_intermediates - if all_ints: - atom_intermediates = torch.stack(all_ints, dim=2) - - return { - "x_denoised": out, - "atom_intermediates": atom_intermediates, - } + return out # =========================================================================== @@ -1499,9 +1442,7 @@ def sample( max_inference_sigma: float | None = 256.0, noise_scale: float | None = None, step_scale: float | None = None, - return_atom_repr: bool = False, use_inference_cache: bool = True, - denoising_early_exit_rmsd: float | None = None, ) -> dict[str, Tensor | None]: """Diffusion sampling (Algorithm 18). @@ -1537,12 +1478,10 @@ def sample( ) x_denoised_prev: Tensor | None = None - diff_atom_intermediates: Tensor | None = None step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) - num_steps = len(step_pairs) - for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(step_pairs): + for sigma_tm, sigma_t, gamma in step_pairs: x, x_denoised_prev = self._center_random_augmentation(x, atom_mask, second_coords=x_denoised_prev) sigma_tm_val = float(sigma_tm.item()) @@ -1550,10 +1489,7 @@ def sample( eps_std = lam * max(t_hat_val**2 - sigma_tm_val**2, 0.0) ** 0.5 x_noisy = x + eps_std * torch.randn_like(x) - is_last_step = step_idx == num_steps - 1 - request_atom_repr = return_atom_repr and (is_last_step or denoising_early_exit_rmsd is not None) - - dm_out = self.diffusion_module( + x_denoised = self.diffusion_module( x_noisy=x_noisy, t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), ref_pos=ref_pos, @@ -1573,14 +1509,9 @@ def sample( sym_id=sym_id, token_attention_mask=token_attention_mask, num_diffusion_samples=num_diffusion_samples, - return_atom_repr=request_atom_repr, inference_cache=inference_cache, ) - x_denoised = dm_out["x_denoised"] - if request_atom_repr: - diff_atom_intermediates = dm_out.get("atom_intermediates") - # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts # to fp32 internally for the SVD/det. x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) @@ -1591,29 +1522,9 @@ def sample( denoised_over_sigma = (x_noisy - x_denoised) / t_hat_val x = x_noisy + eta * (sigma_t_val - t_hat_val) * denoised_over_sigma - # Denoising early-exit: stop when consecutive predictions converge - if denoising_early_exit_rmsd is not None and x_denoised_prev is not None and step_idx >= 1: - aligned = self._weighted_rigid_align( - x_denoised_prev.float(), - x_denoised.float(), - atom_mask, - atom_mask, - ) - diff = (x_denoised.float() - aligned) * atom_mask.unsqueeze(-1) - per_sample_rmsd = (diff.pow(2).sum(dim=(-1, -2)) / atom_mask.sum(dim=-1).clamp(min=1)).sqrt() - if per_sample_rmsd.max().item() < denoising_early_exit_rmsd: - x = x_denoised - x_denoised_prev = x_denoised - break - x_denoised_prev = x_denoised - result: dict[str, Tensor | None] = { - "sample_atom_coords": x, - } - if return_atom_repr: - result["diff_atom_intermediates"] = diff_atom_intermediates - return result + return {"sample_atom_coords": x} # =========================================================================== @@ -1687,7 +1598,7 @@ def forward( [B, L, d_inputs] concatenation of atom encoding, aatype, profile, and deletion_mean. """ - a, _q, _c, _attn_params, _intermediates = self.atom_attention_encoder( + a, _q, _c, _attn_params = self.atom_attention_encoder( ref_pos=ref_pos, atom_attention_mask=atom_attention_mask, ref_space_uid=ref_space_uid, @@ -3043,8 +2954,6 @@ def forward( token_attention_mask=tok_mask, num_diffusion_samples=n_samples, num_sampling_steps=num_sampling_steps, - return_atom_repr=False, - denoising_early_exit_rmsd=None, ) sample_coords = structure_output["sample_atom_coords"] diff --git a/src/transformers/models/esmfold2/protein_utils.py b/src/transformers/models/esmfold2/protein_utils.py index 41609e808bfe..09285e6d8ed8 100644 --- a/src/transformers/models/esmfold2/protein_utils.py +++ b/src/transformers/models/esmfold2/protein_utils.py @@ -24,7 +24,6 @@ MOL_TYPE_PROTEIN = 0 PROTEIN_UNK_RES_TYPE = 22 -MSA_GAP_TOKEN_ID = 1 PROTEIN_RESIDUE_TO_RES_TYPE: dict[str, int] = { "ALA": 2, From 682ff80a13b66c2aa15f6c3085d3a69bac1250d6 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Jun 2026 17:33:40 +0100 Subject: [PATCH 47/70] No more apply_torch_compile --- .../models/esmfold2/modeling_esmfold2.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 589d3eec6fa4..0cac85908605 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -2653,32 +2653,6 @@ def __init__(self, config: ESMFold2Config) -> None: self.post_init() - def apply_torch_compile(self, mode: str = "fixed_seqlen", dynamic: bool | None = None) -> None: - """Compile L²-heavy blocks. ``mode='fixed_seqlen'`` recompiles per L; ``'dynamic_seqlen'`` compiles once.""" - import torch._dynamo - - torch._dynamo.config.cache_size_limit = 512 - torch._dynamo.config.accumulated_cache_size_limit = 512 - # capture_scalar_outputs avoids graph breaks at .item() in atom-attention path. - torch._dynamo.config.capture_scalar_outputs = True - - if dynamic is None: - dynamic = mode == "dynamic_seqlen" - kwargs: dict = {"dynamic": dynamic} - - compile_targets = ( - ESMFold2PairUpdateBlock, - ESMFold2DiffusionTransformer, - ESMFold2DiffusionModule, - ESMFold2MSAEncoderBlock, - ) - - def _maybe_compile(module: nn.Module) -> None: - if isinstance(module, compile_targets): - module.forward = torch.compile(module.forward, **kwargs) - - self.apply(_maybe_compile) - def set_chunk_size(self, chunk_size: int | None) -> None: self.folding_trunk.set_chunk_size(chunk_size) if self.lm_encoder is not None: From 1a2c984a15edf031d1d82f44c47d0e77f196fd91 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Jun 2026 17:41:24 +0100 Subject: [PATCH 48/70] Simplify the obvious einsums but leave the very messy ones --- src/transformers/models/esmfold2/modeling_esmfold2.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 0cac85908605..6c46065028d8 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -455,11 +455,11 @@ def build_3d_rope( ) pos_f32 = ref_pos.float() - spatial_freqs = torch.einsum("bna,k->bnak", pos_f32, spatial_inv_freq) + spatial_freqs = pos_f32.unsqueeze(-1) * spatial_inv_freq spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) uid_f32 = ref_space_uid.float() - uid_freqs = torch.einsum("bn,k->bnk", uid_f32, uid_inv_freq) + uid_freqs = uid_f32.unsqueeze(-1) * uid_inv_freq n_active = n_spatial_total + n_uid_pairs freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) @@ -1387,9 +1387,9 @@ def _center_random_augmentation( second_coords = second_coords - mean r = self._random_rotations(bsz, x.dtype, x.device) - x = torch.einsum("bmd,bds->bms", x, r) + x = x @ r if second_coords is not None: - second_coords = torch.einsum("bmd,bds->bms", second_coords, r) + second_coords = second_coords @ r t = torch.randn_like(x[:, 0:1, :]) x = x + t @@ -1406,7 +1406,7 @@ def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> T mu_gt = (x_gt * w).sum(dim=-2, keepdim=True) / denom x_c = x - mu xgt_c = x_gt - mu_gt - H = torch.einsum("bni,bnj->bij", w * xgt_c, x_c) + H = (w * xgt_c).transpose(-1, -2) @ x_c H32 = H.float() U, _, Vh = torch.linalg.svd(H32, driver="gesvd" if H32.is_cuda else None) det = torch.linalg.det(U @ Vh) From ad9abd2f45a7f97d9e1c6d732e38e5be6d5b2e51 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Jun 2026 18:39:21 +0100 Subject: [PATCH 49/70] Config cleanup, stop passing naked kwargs around --- .../models/esmc/configuration_esmc.py | 11 + src/transformers/models/esmc/modeling_esmc.py | 117 ++---- src/transformers/models/esmc/modular_esmc.py | 117 ++---- .../models/esmfold2/configuration_esmfold2.py | 10 +- .../models/esmfold2/modeling_esmfold2.py | 392 +++++------------- .../models/esmfold2/test_modeling_esmfold2.py | 4 +- 6 files changed, 182 insertions(+), 469 deletions(-) diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py index 891fdf5f439d..3b41d2cdb302 100644 --- a/src/transformers/models/esmc/configuration_esmc.py +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -35,6 +35,14 @@ class ESMCConfig(PreTrainedConfig): Dropout ratio for the classification head. rope_theta (`float`, *optional*, defaults to 10000.0): The base period of the rotary position embeddings (RoPE). + expansion_ratio (`float`, *optional*, defaults to `8/3`): + Hidden-dim expansion ratio for the SwiGLU feed-forward network (the hidden + size is rounded up to a multiple of 256). + qk_layernorm (`bool`, *optional*, defaults to `True`): + Whether to apply LayerNorm to queries and keys before computing attention. + scale_residue (`bool`, *optional*, defaults to `True`): + Whether to apply ESM3 residual scaling (`1 / sqrt(num_hidden_layers / 36)` + per block) to stabilise deep networks. Examples: @@ -68,6 +76,9 @@ class ESMCConfig(PreTrainedConfig): initializer_range: float | None = 0.02 classifier_dropout: float | None = 0.1 rope_theta: float | None = 10000.0 + expansion_ratio: float | None = 8 / 3 + qk_layernorm: bool | None = True + scale_residue: bool | None = True tie_word_embeddings: bool | None = False diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 9365b7fe401a..444413579f43 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -233,26 +233,15 @@ def eager_attention_forward( class ESMCAttention(nn.Module): """Multi-head self-attention with QK LayerNorm and RoPE. - Args: - d_model: Model hidden dimension. - n_heads: Number of attention heads. - bias: Whether to use bias in linear layers. - qk_layernorm: Whether to apply LayerNorm to queries and keys before - computing attention scores. + All dims/flags are read from ``config`` (``d_model``/``n_heads``/ + ``qk_layernorm``). ESMC is bias-free, so the linears carry no bias. """ - def __init__( - self, - config: ESMCConfig, - d_model: int, - n_heads: int, - bias: bool = False, - qk_layernorm: bool = True, - ): + def __init__(self, config: ESMCConfig): super().__init__() - if bias: - raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") self.config = config + d_model = config.d_model + n_heads = config.n_heads self.d_model = d_model self.n_heads = n_heads self.d_head = d_model // n_heads @@ -268,12 +257,12 @@ def __init__( # :class:`ESMCLayer` (``attn_norm``); the published checkpoint's fused # ``attn.layernorm_qkv`` / ``attn.out_proj`` map onto these via # ``conversion_mapping.py``. - self.Wqkv = nn.Linear(d_model, d_model * 3, bias=bias) - self.Wo = nn.Linear(d_model, d_model, bias=bias) + self.Wqkv = nn.Linear(d_model, d_model * 3, bias=False) + self.Wo = nn.Linear(d_model, d_model, bias=False) - if qk_layernorm: - self.q_ln = nn.LayerNorm(d_model, bias=bias) - self.k_ln = nn.LayerNorm(d_model, bias=bias) + if config.qk_layernorm: + self.q_ln = nn.LayerNorm(d_model, bias=False) + self.k_ln = nn.LayerNorm(d_model, bias=False) else: self.q_ln = nn.Identity() self.k_ln = nn.Identity() @@ -339,40 +328,23 @@ def forward( class ESMCLayer(nn.Module): """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. - Args: - d_model: Hidden dimension. - n_heads: Number of attention heads. - use_flash_attn: Use Flash Attention 2 kernel if available. - bias: Whether linear layers include bias terms. - expansion_ratio: Hidden-dim expansion ratio for the FFN. - residue_scaling_factor: Scales residual connections to stabilise deep - networks (``1 / sqrt(n_layers / 36)`` is the ESM3 scheme). - qk_layernorm: Whether to apply QK LayerNorm in attention. + All dims/flags are read from ``config``. The ESM3 residue-scaling factor + (``sqrt(num_hidden_layers / 36)`` when ``config.scale_residue``) is derived + from the config here rather than threaded in. """ - def __init__( - self, - config: ESMCConfig, - d_model: int, - n_heads: int, - bias: bool = False, - expansion_ratio: float = 8 / 3, - residue_scaling_factor: float = 1.0, - qk_layernorm: bool = True, - ): + def __init__(self, config: ESMCConfig): super().__init__() - if bias: - raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") - + d_model = config.d_model # Pre-norm layout (cf. ModernBERT): the LayerNorms that the published ESMC # checkpoint fuses into ``attn.layernorm_qkv`` / ``ffn`` live here as # ``attn_norm`` / ``mlp_norm`` (remapped in ``conversion_mapping.py``). self.attn_norm = nn.LayerNorm(d_model) - self.attn = ESMCAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + self.attn = ESMCAttention(config) self.mlp_norm = nn.LayerNorm(d_model) - self.mlp = ESMCMLP(d_model, expansion_ratio) + self.mlp = ESMCMLP(d_model, config.expansion_ratio) - self.scaling_factor = residue_scaling_factor + self.scaling_factor = math.sqrt(config.n_layers / 36) if config.scale_residue else 1.0 def forward( self, @@ -409,47 +381,13 @@ def forward( class ESMCEncoder(nn.Module): - """Stack of :class:`ESMCLayer` layers with a final LayerNorm. + """Stack of :class:`ESMCLayer` layers with a final LayerNorm. All dims/flags + are read from ``config``.""" - Args: - d_model: Hidden dimension. - n_heads: Number of attention heads. - n_layers: Number of transformer blocks. - scale_residue: When ``True`` apply ESM3 residue scaling - ``sqrt(n_layers / 36)`` to each block. - bias: Bias flag forwarded to every sub-module. - qk_layernorm: QK LayerNorm flag forwarded to every block. - expansion_ratio: FFN expansion ratio. - use_flash_attn: Use Flash Attention 2 kernel when available. - """ - - def __init__( - self, - config: ESMCConfig, - d_model: int, - n_heads: int, - n_layers: int, - scale_residue: bool = True, - bias: bool = False, - qk_layernorm: bool = True, - expansion_ratio: float = 8 / 3, - ): + def __init__(self, config: ESMCConfig): super().__init__() - self.blocks = nn.ModuleList( - [ - ESMCLayer( - config, - d_model, - n_heads, - residue_scaling_factor=math.sqrt(n_layers / 36) if scale_residue else 1.0, - expansion_ratio=expansion_ratio, - bias=bias, - qk_layernorm=qk_layernorm, - ) - for _ in range(n_layers) - ] - ) - self.norm = nn.LayerNorm(d_model, bias=False) + self.blocks = nn.ModuleList([ESMCLayer(config) for _ in range(config.n_layers)]) + self.norm = nn.LayerNorm(config.d_model, bias=False) def forward( self, @@ -545,12 +483,7 @@ def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) self.rotary_emb = ESMCRotaryEmbedding(config) - self.transformer = ESMCEncoder( - config, - config.d_model, - config.n_heads, - config.n_layers, - ) + self.transformer = ESMCEncoder(config) self.post_init() def get_input_embeddings(self) -> nn.Embedding: @@ -603,7 +536,7 @@ def forward( output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states ) output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions - return_dict = return_dict if return_dict is not None else self.config.use_return_dict + return_dict = return_dict if return_dict is not None else self.config.return_dict # Collect per-layer hidden states only when the caller asks for them. layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) if output_hidden_states else [] diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 5d4a010fa89e..d9516f65bb06 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -123,26 +123,15 @@ def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: class ESMCAttention(nn.Module): """Multi-head self-attention with QK LayerNorm and RoPE. - Args: - d_model: Model hidden dimension. - n_heads: Number of attention heads. - bias: Whether to use bias in linear layers. - qk_layernorm: Whether to apply LayerNorm to queries and keys before - computing attention scores. + All dims/flags are read from ``config`` (``d_model``/``n_heads``/ + ``qk_layernorm``). ESMC is bias-free, so the linears carry no bias. """ - def __init__( - self, - config: ESMCConfig, - d_model: int, - n_heads: int, - bias: bool = False, - qk_layernorm: bool = True, - ): + def __init__(self, config: ESMCConfig): super().__init__() - if bias: - raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") self.config = config + d_model = config.d_model + n_heads = config.n_heads self.d_model = d_model self.n_heads = n_heads self.d_head = d_model // n_heads @@ -158,12 +147,12 @@ def __init__( # :class:`ESMCLayer` (``attn_norm``); the published checkpoint's fused # ``attn.layernorm_qkv`` / ``attn.out_proj`` map onto these via # ``conversion_mapping.py``. - self.Wqkv = nn.Linear(d_model, d_model * 3, bias=bias) - self.Wo = nn.Linear(d_model, d_model, bias=bias) + self.Wqkv = nn.Linear(d_model, d_model * 3, bias=False) + self.Wo = nn.Linear(d_model, d_model, bias=False) - if qk_layernorm: - self.q_ln = nn.LayerNorm(d_model, bias=bias) - self.k_ln = nn.LayerNorm(d_model, bias=bias) + if config.qk_layernorm: + self.q_ln = nn.LayerNorm(d_model, bias=False) + self.k_ln = nn.LayerNorm(d_model, bias=False) else: self.q_ln = nn.Identity() self.k_ln = nn.Identity() @@ -229,40 +218,23 @@ def forward( class ESMCLayer(nn.Module): """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. - Args: - d_model: Hidden dimension. - n_heads: Number of attention heads. - use_flash_attn: Use Flash Attention 2 kernel if available. - bias: Whether linear layers include bias terms. - expansion_ratio: Hidden-dim expansion ratio for the FFN. - residue_scaling_factor: Scales residual connections to stabilise deep - networks (``1 / sqrt(n_layers / 36)`` is the ESM3 scheme). - qk_layernorm: Whether to apply QK LayerNorm in attention. + All dims/flags are read from ``config``. The ESM3 residue-scaling factor + (``sqrt(num_hidden_layers / 36)`` when ``config.scale_residue``) is derived + from the config here rather than threaded in. """ - def __init__( - self, - config: ESMCConfig, - d_model: int, - n_heads: int, - bias: bool = False, - expansion_ratio: float = 8 / 3, - residue_scaling_factor: float = 1.0, - qk_layernorm: bool = True, - ): + def __init__(self, config: ESMCConfig): super().__init__() - if bias: - raise ValueError("ESMC was trained with bias=False; bias=True is not supported.") - + d_model = config.d_model # Pre-norm layout (cf. ModernBERT): the LayerNorms that the published ESMC # checkpoint fuses into ``attn.layernorm_qkv`` / ``ffn`` live here as # ``attn_norm`` / ``mlp_norm`` (remapped in ``conversion_mapping.py``). self.attn_norm = nn.LayerNorm(d_model) - self.attn = ESMCAttention(config, d_model, n_heads, bias=bias, qk_layernorm=qk_layernorm) + self.attn = ESMCAttention(config) self.mlp_norm = nn.LayerNorm(d_model) - self.mlp = ESMCMLP(d_model, expansion_ratio) + self.mlp = ESMCMLP(d_model, config.expansion_ratio) - self.scaling_factor = residue_scaling_factor + self.scaling_factor = math.sqrt(config.n_layers / 36) if config.scale_residue else 1.0 def forward( self, @@ -299,47 +271,13 @@ def forward( class ESMCEncoder(nn.Module): - """Stack of :class:`ESMCLayer` layers with a final LayerNorm. - - Args: - d_model: Hidden dimension. - n_heads: Number of attention heads. - n_layers: Number of transformer blocks. - scale_residue: When ``True`` apply ESM3 residue scaling - ``sqrt(n_layers / 36)`` to each block. - bias: Bias flag forwarded to every sub-module. - qk_layernorm: QK LayerNorm flag forwarded to every block. - expansion_ratio: FFN expansion ratio. - use_flash_attn: Use Flash Attention 2 kernel when available. - """ + """Stack of :class:`ESMCLayer` layers with a final LayerNorm. All dims/flags + are read from ``config``.""" - def __init__( - self, - config: ESMCConfig, - d_model: int, - n_heads: int, - n_layers: int, - scale_residue: bool = True, - bias: bool = False, - qk_layernorm: bool = True, - expansion_ratio: float = 8 / 3, - ): + def __init__(self, config: ESMCConfig): super().__init__() - self.blocks = nn.ModuleList( - [ - ESMCLayer( - config, - d_model, - n_heads, - residue_scaling_factor=math.sqrt(n_layers / 36) if scale_residue else 1.0, - expansion_ratio=expansion_ratio, - bias=bias, - qk_layernorm=qk_layernorm, - ) - for _ in range(n_layers) - ] - ) - self.norm = nn.LayerNorm(d_model, bias=False) + self.blocks = nn.ModuleList([ESMCLayer(config) for _ in range(config.n_layers)]) + self.norm = nn.LayerNorm(config.d_model, bias=False) def forward( self, @@ -435,12 +373,7 @@ def __init__(self, config: ESMCConfig): super().__init__(config) self.embed = nn.Embedding(config.vocab_size, config.d_model) self.rotary_emb = ESMCRotaryEmbedding(config) - self.transformer = ESMCEncoder( - config, - config.d_model, - config.n_heads, - config.n_layers, - ) + self.transformer = ESMCEncoder(config) self.post_init() def get_input_embeddings(self) -> nn.Embedding: @@ -493,7 +426,7 @@ def forward( output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states ) output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions - return_dict = return_dict if return_dict is not None else self.config.use_return_dict + return_dict = return_dict if return_dict is not None else self.config.return_dict # Collect per-layer hidden states only when the caller asks for them. layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) if output_hidden_states else [] diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 760360314314..90b02fa089e3 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -88,16 +88,18 @@ class AtomAttentionConfig(PreTrainedConfig): @strict class FoldingTrunkConfig(PreTrainedConfig): - """Config for a pairwise folding trunk stack.""" + """Config for a pairwise folding trunk stack. + + The trunk's ``ESMFold2PairUpdateBlock`` is attention-free (triangle + multiplication + transition), so only ``num_hidden_layers`` is read; the + pair dim comes from the parent's ``d_pair``. + """ attribute_map = { "n_layers": "num_hidden_layers", - "n_heads": "num_attention_heads", } num_hidden_layers: int | None = 24 - num_attention_heads: int | None = 8 - dropout: float | None = 0.0 @strict diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 6c46065028d8..e589ad54b9d6 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -263,6 +263,32 @@ def qk_norm(x: Tensor) -> Tensor: return F.rms_norm(x, (x.size(-1),)) +def _resolve_atom_config(config: ESMFold2Config, structure_prediction: bool): + """Resolve the SWA atom-stack hyperparameters for a given call site. + + The atom transformer is the same architecture in two places: the inputs + embedder (``structure_prediction=False``) and the diffusion module + (``structure_prediction=True``). Its I/O dims and block/head counts come + from the call-site sub-config; the window, expansion ratio and 3D-RoPE + settings always come from ``config.inputs.atom_encoder`` — the diffusion + module reused those (its atom encoder was built with the same window/RoPE and + a fixed ``expansion_ratio=2``, which is ``AtomAttentionConfig``'s default). + + Every module in the atom stack (attention/block/transformer/encoder/decoder) + takes only ``(config, structure_prediction)`` and derives its own dims from + this, so no scalar dims are threaded between them. + + Returns ``(d_atom, d_token, n_blocks, n_heads, swa)`` where ``swa`` is the + ``AtomAttentionConfig`` carrying ``swa_window_size`` / ``expansion_ratio`` / + the RoPE settings. + """ + swa = config.inputs.atom_encoder + if structure_prediction: + dm = config.structure_head.diffusion_module + return dm.c_atom, dm.c_token, dm.atom_num_blocks, dm.atom_num_heads, swa + return swa.d_atom, swa.d_token, swa.n_blocks, swa.n_heads, swa + + # =========================================================================== # ESMFold2SWA3DRoPEAttention # =========================================================================== @@ -276,18 +302,20 @@ class ESMFold2SWA3DRoPEAttention(nn.Module): with the sliding window expressed as an additive attention mask. The custom flash-attention path (native bidirectional ``window_size``, plus varlen for packed inputs) is kept as an opt-in backend, selected when - ``_attn_implementation == "flash_attention_2"``. ``config`` is attached by - the parent ``ESMFold2Model`` after construction; it is ``None`` (→ ``sdpa``) - when the module is used standalone. + ``_attn_implementation == "flash_attention_2"``. The shared ``config`` is + passed in at construction, so ``set_attn_implementation()`` stays live (it + mutates the same object); dims/window are derived from + ``(config, structure_prediction)`` via :func:`_resolve_atom_config`. """ - def __init__(self, d_model: int, n_heads: int, half_window: int = 64) -> None: + def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - self.config = None + d_model, _d_token, _n_blocks, n_heads, swa = _resolve_atom_config(config, structure_prediction) + self.config = config self.n_heads = n_heads self.head_dim = d_model // n_heads self.scale = self.head_dim**-0.5 - self.half_window = half_window + self.half_window = swa.swa_window_size // 2 # No grouped-query attention; identity repeat keeps the interface happy. self.num_key_value_groups = 1 # Bidirectional encoder: never let the sdpa/flash interface default to @@ -315,7 +343,7 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: if q.dtype not in (torch.float16, torch.bfloat16): q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() - attn_impl = self.config._attn_implementation if self.config is not None else "sdpa" + attn_impl = self.config._attn_implementation use_flash = attn_impl == "flash_attention_2" and is_flash_attn_2_available() if use_flash and len(attention_params) > 2: @@ -400,19 +428,14 @@ class ESMFold2SWAAtomBlock(nn.Module): Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight """ - def __init__( - self, - d_atom: int, - n_heads: int, - half_window: int = 64, - expansion_ratio: int = 2, - ) -> None: + def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() + d_atom, _d_token, _n_blocks, _n_heads, swa = _resolve_atom_config(config, structure_prediction) # adaln-Zero gate; zero-init lives in ESMFold2PreTrainedModel._init_weights. self.adaln_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_atom, 6 * d_atom, bias=False)) - self.attn = ESMFold2SWA3DRoPEAttention(d_atom, n_heads, half_window=half_window) - self.ffn = ESMFold2SwiGLUFFN(d_atom, expansion_ratio) + self.attn = ESMFold2SWA3DRoPEAttention(config, structure_prediction) + self.ffn = ESMFold2SwiGLUFFN(d_atom, swa.expansion_ratio) def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: mod = self.adaln_modulation(c_l) @@ -476,37 +499,17 @@ def build_3d_rope( class ESMFold2SWAAtomTransformer(nn.Module): """Stack of SWAAtomBlocks.""" - def __init__( - self, - d_atom: int = 128, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: + def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - self.swa_window_size = swa_window_size + d_atom, _d_token, n_blocks, n_heads, swa = _resolve_atom_config(config, structure_prediction) + self.swa_window_size = swa.swa_window_size self.head_dim = d_atom // n_heads - self.spatial_rope_base_frequency = spatial_rope_base_frequency - self.n_spatial_rope_pairs_per_axis = n_spatial_rope_pairs_per_axis - self.n_uid_rope_pairs = n_uid_rope_pairs - self.uid_rope_base_frequency = uid_rope_base_frequency + self.spatial_rope_base_frequency = swa.spatial_rope_base_frequency + self.n_spatial_rope_pairs_per_axis = swa.n_spatial_rope_pairs_per_axis + self.n_uid_rope_pairs = swa.n_uid_rope_pairs + self.uid_rope_base_frequency = swa.uid_rope_base_frequency - self.blocks = nn.ModuleList( - [ - ESMFold2SWAAtomBlock( - d_atom=d_atom, - n_heads=n_heads, - half_window=swa_window_size // 2, - expansion_ratio=expansion_ratio, - ) - for _ in range(n_blocks) - ] - ) + self.blocks = nn.ModuleList([ESMFold2SWAAtomBlock(config, structure_prediction) for _ in range(n_blocks)]) def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: return build_3d_rope( @@ -579,30 +582,15 @@ def scatter_atom_to_token( class ESMFold2AtomEncoder(nn.Module): """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. - Args: - d_atom: atom hidden dim - d_token: token dim for atom_to_token aggregation - n_blocks, n_heads, swa_window_size, expansion_ratio: transformer params - structure_prediction: if True, creates coords_linear and uses full d_token - spatial_rope_base_frequency, n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs, uid_rope_base_frequency: 3D RoPE config + ``structure_prediction=True`` (diffusion module) adds ``coords_linear`` and + aggregates to the full ``d_token``; ``False`` (inputs embedder) omits coords + and aggregates to ``d_token // 2``. All dims/hyperparameters are read from + ``config`` via :func:`_resolve_atom_config`. """ - def __init__( - self, - d_atom: int = 128, - d_token: int = 768, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - structure_prediction: bool = True, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: + def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() + d_atom, d_token, _n_blocks, _n_heads, _swa = _resolve_atom_config(config, structure_prediction) self.d_atom = d_atom self.d_token = d_token self.structure_prediction = structure_prediction @@ -613,17 +601,7 @@ def __init__( if structure_prediction: self.coords_linear = nn.Linear(6, d_atom, bias=False) - self.atom_transformer = ESMFold2SWAAtomTransformer( - d_atom=d_atom, - n_blocks=n_blocks, - n_heads=n_heads, - swa_window_size=swa_window_size, - expansion_ratio=expansion_ratio, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) + self.atom_transformer = ESMFold2SWAAtomTransformer(config, structure_prediction) # Output aggregation: d_token for structure prediction, d_token//2 for inputs out_dim = d_token if structure_prediction else d_token // 2 @@ -745,35 +723,18 @@ def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> T class ESMFold2AtomDecoder(nn.Module): - """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear.""" + """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear. - def __init__( - self, - d_atom: int = 128, - d_token: int = 768, - n_blocks: int = 3, - n_heads: int = 4, - swa_window_size: int = 128, - expansion_ratio: int = 2, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: + Only used inside the diffusion module, so its atom dims are always the + structure-prediction (diffusion) ones (:func:`_resolve_atom_config`). + """ + + def __init__(self, config: ESMFold2Config) -> None: super().__init__() + d_atom, d_token, _n_blocks, _n_heads, _swa = _resolve_atom_config(config, structure_prediction=True) self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) - self.atom_transformer = ESMFold2SWAAtomTransformer( - d_atom=d_atom, - n_blocks=n_blocks, - n_heads=n_heads, - swa_window_size=swa_window_size, - expansion_ratio=expansion_ratio, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) + self.atom_transformer = ESMFold2SWAAtomTransformer(config, structure_prediction=True) self.norm = ESMFold2LayerNorm(d_atom) self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) @@ -945,17 +906,15 @@ def forward(self, a: Tensor, s: Tensor) -> Tensor: class ESMFold2DiffusionTransformer(nn.Module): """Diffusion denoising transformer with attention pair bias.""" - def __init__( - self, - d_model: int, - d_pair: int, - num_heads: int, - num_blocks: int, - d_cond: int | None = None, - transition_multiplier: int = 2, - ) -> None: + def __init__(self, config: ESMFold2Config) -> None: super().__init__() - d_cond = d_cond or d_model + dm = config.structure_head.diffusion_module + d_model = dm.c_token + d_pair = dm.c_z + num_heads = dm.token_num_heads + num_blocks = dm.token_num_blocks + d_cond = dm.c_token + transition_multiplier = dm.transition_multiplier self.attn_blocks = nn.ModuleList( [ @@ -1023,36 +982,30 @@ def forward( class ESMFold2DiffusionConditioning(nn.Module): """Conditions pair and single representations on noise timestep.""" - def __init__( - self, - c_z: int = 256, - c_s: int = 768, - c_s_inputs: int = 451, - sigma_data: float = 16.0, - fourier_dim: int = 256, - transition_multiplier: int = 2, - layer_norm_eps: float = 1e-5, - ) -> None: + def __init__(self, config: ESMFold2Config) -> None: super().__init__() - self.sigma_data = float(sigma_data) + dm = config.structure_head.diffusion_module + c_z = dm.c_z + c_s = dm.c_token # the conditioning's single output is sized to c_token + c_s_inputs = dm.c_s_inputs + fourier_dim = dm.fourier_dim + transition_multiplier = dm.transition_multiplier + # The norms/transitions use their default eps (1e-5), as before. + self.sigma_data = float(dm.sigma_data) self.c_z = c_z self.c_s = c_s self.c_s_inputs = c_s_inputs - self.z_input_norm = ESMFold2LayerNorm(2 * c_z, eps=layer_norm_eps) + self.z_input_norm = ESMFold2LayerNorm(2 * c_z) self.z_proj = nn.Linear(2 * c_z, c_z, bias=False) - self.z_transitions = nn.ModuleList( - [ESMFold2TransitionLayer(c_z, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] - ) + self.z_transitions = nn.ModuleList([ESMFold2TransitionLayer(c_z, n=transition_multiplier) for _ in range(2)]) - self.s_input_norm = ESMFold2LayerNorm(c_s_inputs, eps=layer_norm_eps) + self.s_input_norm = ESMFold2LayerNorm(c_s_inputs) self.s_proj = nn.Linear(c_s_inputs, c_s, bias=False) self.fourier = ESMFold2FourierEmbedding(fourier_dim) - self.noise_norm = ESMFold2LayerNorm(fourier_dim, eps=layer_norm_eps) + self.noise_norm = ESMFold2LayerNorm(fourier_dim) self.noise_proj = nn.Linear(fourier_dim, c_s, bias=False) - self.s_transitions = nn.ModuleList( - [ESMFold2TransitionLayer(c_s, n=transition_multiplier, eps=layer_norm_eps) for _ in range(2)] - ) + self.s_transitions = nn.ModuleList([ESMFold2TransitionLayer(c_s, n=transition_multiplier) for _ in range(2)]) def forward( self, @@ -1114,78 +1067,25 @@ def forward( class ESMFold2DiffusionModule(nn.Module): """Diffusion denoising module for structure prediction.""" - def __init__( - self, - c_atom: int = 128, - c_token: int = 768, - c_z: int = 256, - c_s_inputs: int = 451, - sigma_data: float = 16.0, - fourier_dim: int = 256, - atom_num_blocks: int = 3, - atom_num_heads: int = 4, - token_num_blocks: int = 12, - token_num_heads: int = 16, - transition_multiplier: int = 2, - swa_window_size: int = 128, - spatial_rope_base_frequency: float = 20.0, - n_spatial_rope_pairs_per_axis: int = 2, - n_uid_rope_pairs: int = 10, - uid_rope_base_frequency: float = 10000.0, - ) -> None: + def __init__(self, config: ESMFold2Config) -> None: super().__init__() - self.sigma_data = float(sigma_data) - - self.conditioning = ESMFold2DiffusionConditioning( - c_z=c_z, - c_s=c_token, # conditioning s output is c_token - c_s_inputs=c_s_inputs, - sigma_data=sigma_data, - fourier_dim=fourier_dim, - transition_multiplier=transition_multiplier, - ) + dm = config.structure_head.diffusion_module + c_token = dm.c_token + self.sigma_data = float(dm.sigma_data) + + self.conditioning = ESMFold2DiffusionConditioning(config) # Atom encoder (structure_prediction=True, with coords_linear) - self.atom_encoder = ESMFold2AtomEncoder( - d_atom=c_atom, - d_token=c_token, - n_blocks=atom_num_blocks, - n_heads=atom_num_heads, - swa_window_size=swa_window_size, - expansion_ratio=2, - structure_prediction=True, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) + self.atom_encoder = ESMFold2AtomEncoder(config, structure_prediction=True) # Atom decoder - self.atom_decoder = ESMFold2AtomDecoder( - d_atom=c_atom, - d_token=c_token, - n_blocks=atom_num_blocks, - n_heads=atom_num_heads, - swa_window_size=swa_window_size, - expansion_ratio=2, - spatial_rope_base_frequency=spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=n_uid_rope_pairs, - uid_rope_base_frequency=uid_rope_base_frequency, - ) + self.atom_decoder = ESMFold2AtomDecoder(config) # zero-init lives in ESMFold2PreTrainedModel._init_weights. self.s_to_token = nn.Linear(c_token, c_token, bias=False) # Token transformer (ESMFold2DiffusionTransformer with pair bias) - self.token_transformer = ESMFold2DiffusionTransformer( - d_model=c_token, - d_pair=c_z, - num_heads=token_num_heads, - num_blocks=token_num_blocks, - d_cond=c_token, - transition_multiplier=transition_multiplier, - ) + self.token_transformer = ESMFold2DiffusionTransformer(config) self.s_step_norm = ESMFold2LayerNorm(c_token) self.token_norm = ESMFold2LayerNorm(c_token) @@ -1295,31 +1195,12 @@ class ESMFold2DiffusionStructureHead(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() - dm = config.structure_head.diffusion_module - swa_cfg = config.inputs.atom_encoder sh = config.structure_head - self.diffusion_module = ESMFold2DiffusionModule( - c_atom=dm.c_atom, - c_token=dm.c_token, - c_z=dm.c_z, - c_s_inputs=dm.c_s_inputs, - sigma_data=dm.sigma_data, - fourier_dim=dm.fourier_dim, - atom_num_blocks=dm.atom_num_blocks, - atom_num_heads=dm.atom_num_heads, - token_num_blocks=dm.token_num_blocks, - token_num_heads=dm.token_num_heads, - transition_multiplier=dm.transition_multiplier, - swa_window_size=swa_cfg.swa_window_size, - spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, - uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, - ) + self.diffusion_module = ESMFold2DiffusionModule(config) # Sampling hyperparameters - self.sigma_data = dm.sigma_data + self.sigma_data = sh.diffusion_module.sigma_data self.gamma_0 = sh.gamma_0 self.gamma_min = sh.gamma_min self.noise_scale = sh.noise_scale @@ -1563,21 +1444,8 @@ class ESMFold2InputsEmbedder(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() - swa_cfg = config.inputs.atom_encoder - - self.atom_attention_encoder = ESMFold2AtomEncoder( - d_atom=swa_cfg.d_atom, - d_token=swa_cfg.d_token, - n_blocks=swa_cfg.n_blocks, - n_heads=swa_cfg.n_heads, - swa_window_size=swa_cfg.swa_window_size, - expansion_ratio=swa_cfg.expansion_ratio, - structure_prediction=False, # no coords_linear - spatial_rope_base_frequency=swa_cfg.spatial_rope_base_frequency, - n_spatial_rope_pairs_per_axis=swa_cfg.n_spatial_rope_pairs_per_axis, - n_uid_rope_pairs=swa_cfg.n_uid_rope_pairs, - uid_rope_base_frequency=swa_cfg.uid_rope_base_frequency, - ) + # structure_prediction=False: no coords_linear, aggregates to d_token // 2. + self.atom_attention_encoder = ESMFold2AtomEncoder(config, structure_prediction=False) def forward( self, @@ -2629,27 +2497,9 @@ def __init__(self, config: ESMFold2Config) -> None: self.distogram_head = nn.Linear(d_pair, config.structure_head.distogram_bins, bias=True) self.confidence_head = ESMFold2ConfidenceHead(config) - msa_cfg = config.msa_encoder self.msa_encoder = None - if msa_cfg.enabled: - self.msa_encoder = ESMFold2MSAEncoder( - d_msa=msa_cfg.d_msa, - d_pair=d_pair, - d_inputs=d_inputs, - d_hidden=msa_cfg.d_hidden, - n_layers=msa_cfg.n_layers, - n_heads_msa=msa_cfg.n_heads_msa, - msa_head_width=msa_cfg.msa_head_width, - ) - - # ESMFold2SWA3DRoPEAttention modules live deep in the atom encoders/decoders and - # are built from explicit dims, so give each a handle to the model config: - # their forward dispatches the plain-attention core through the v5 - # attention interface (config._attn_implementation), staying live under - # set_attn_implementation() since the config object is shared. - for module in self.modules(): - if isinstance(module, ESMFold2SWA3DRoPEAttention): - module.config = self.config + if config.msa_encoder.enabled: + self.msa_encoder = ESMFold2MSAEncoder(config) self.post_init() @@ -2983,16 +2833,14 @@ def output_to_pdb(output: ESMFold2Output, features: dict[str, Tensor]) -> str: class ESMFold2MSAEncoderBlock(nn.Module): """One MSA encoder block: OPM into pair, MSA pair-weighted averaging, triangle update.""" - def __init__( - self, - d_msa: int, - d_pair: int, - d_hidden: int, - n_heads_msa: int, - msa_head_width: int, - is_final_block: bool = False, - ) -> None: + def __init__(self, config: ESMFold2Config, is_final_block: bool = False) -> None: super().__init__() + me = config.msa_encoder + d_msa = me.d_msa + d_pair = config.d_pair + d_hidden = me.d_hidden + n_heads_msa = me.n_heads_msa + msa_head_width = me.msa_head_width self.is_final_block = is_final_block self.outer_product_mean = ESMFold2OuterProductMean(d_msa, d_hidden, d_pair) if not is_final_block: @@ -3032,31 +2880,17 @@ def forward( class ESMFold2MSAEncoder(nn.Module): """Stack of [`ESMFold2MSAEncoderBlock`] layers that conditions the pair on an MSA.""" - def __init__( - self, - d_msa: int, - d_pair: int, - d_inputs: int, - d_hidden: int = 32, - n_layers: int = 4, - n_heads_msa: int = 8, - msa_head_width: int = 16, - ) -> None: + def __init__(self, config: ESMFold2Config) -> None: super().__init__() + me = config.msa_encoder + d_msa = me.d_msa + d_inputs = config.inputs.d_inputs + n_layers = me.n_layers + # 35 = NUM_RES_TYPES (33) one-hot + has_deletion + deletion_value. self.embed = nn.Linear(35, d_msa, bias=False) self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) self.blocks = nn.ModuleList( - [ - ESMFold2MSAEncoderBlock( - d_msa=d_msa, - d_pair=d_pair, - d_hidden=d_hidden, - n_heads_msa=n_heads_msa, - msa_head_width=msa_head_width, - is_final_block=(i == n_layers - 1), - ) - for i in range(n_layers) - ] + [ESMFold2MSAEncoderBlock(config, is_final_block=(i == n_layers - 1)) for i in range(n_layers)] ) def set_chunk_size(self, chunk_size: int | None) -> None: diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index 8f97600d11b4..22eec92cb789 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -70,7 +70,7 @@ def get_tiny_config(**overrides) -> "ESMFold2Config": "n_uid_rope_pairs": 1, }, }, - "folding_trunk": {"n_layers": 1, "n_heads": 2}, + "folding_trunk": {"n_layers": 1}, "structure_head": { "diffusion_module": { "c_atom": 16, @@ -89,7 +89,7 @@ def get_tiny_config(**overrides) -> "ESMFold2Config": "num_pde_bins": 4, "num_pae_bins": 4, "distogram_bins": 8, - "folding_trunk": {"n_layers": 1, "n_heads": 2}, + "folding_trunk": {"n_layers": 1}, }, "parcae": {"coda_n_layers": 1}, "lm_encoder": {"n_layers": 1}, From 4b0e39d74c4f15d4b359fda694e92ddb9fee2586 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 13:51:22 +0100 Subject: [PATCH 50/70] Small docs cleanups --- docs/source/en/model_doc/esmc.md | 14 +++++++------- docs/source/en/model_doc/esmfold2.md | 16 ++++++---------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 5e3fbe6aa186..be1b55c3e6e3 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -19,17 +19,17 @@ rendered properly in your Markdown viewer. ## Overview -ESMC (ESM Cambrian) is a family of protein language models released by [EvolutionaryScale](https://www.evolutionaryscale.ai/). -It is a bidirectional Transformer encoder trained with a masked-language-modelling objective over amino-acid sequences, -using rotary position embeddings (RoPE), query/key LayerNorm and a SwiGLU feed-forward network. Like [ESM-2](./esm), -ESMC produces per-residue representations that are useful for downstream protein modelling tasks; unlike ESMFold it is a -sequence model only and does not predict structure. +ESMC (ESM Cambrian) is a family of protein language models released by [BioHub](https://biohub.org/). +It is a bidirectional Transformer encoder trained with a masked-language-modelling objective over amino-acid sequences. +Like [ESM-2](./esm), ESMC produces per-residue representations that are useful for downstream protein modelling tasks. + +ESMC is suitable for fine-tuning on protein classification or token classification tasks. It is also used as the +backbone of [ESMFold2](./esmfold2), where it generates representations that are used as input to the folding head. Pre-trained checkpoints are available on the Hugging Face Hub, including [`biohub/ESMC-300M`](https://huggingface.co/biohub/ESMC-300M), [`biohub/ESMC-600M`](https://huggingface.co/biohub/ESMC-600M) and -[`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B). The original code is available in the -[`evolutionaryscale/esm`](https://github.com/evolutionaryscale/esm) repository. +[`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B). ## Usage example diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 9d73f99c21ff..476f70550870 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -22,15 +22,10 @@ rendered properly in your Markdown viewer. ESMFold2 is an all-atom protein structure prediction model. It predicts 3D coordinates and per-residue confidence (pLDDT, PAE, PDE) directly from an amino-acid sequence, using the [ESMC](./esmc) protein language model as its backbone. The architecture combines a sliding-window atom encoder with 3D rotary position embeddings, a pairwise -folding trunk applied iteratively (the "parcae" recurrence), a diffusion-based structure head, and a confidence head. +folding trunk applied iteratively, a diffusion-based structure head, and a confidence head. -The ESMC backbone is a bundled submodule: it is built from `config.esmc_config` (defaulting to the ESMC-6B -configuration) and its weights are part of the ESMFold2 checkpoint under `esmc.*`, so a single -[`ESMFold2Model.from_pretrained`](#transformers.ESMFold2Model) loads the whole model — no separate backbone download. -You can still bypass the backbone by supplying precomputed `lm_hidden_states` to `forward`. - -The model checkpoint is available on the Hugging Face Hub at -[`biohub/ESMFold2`](https://huggingface.co/biohub/ESMFold2). +The model checkpoints are available on the Hugging Face Hub at +[`biohub/ESMFold2`](https://huggingface.co/biohub/ESMFold2) and [`biohub/ESMFold2-Fast`](https://huggingface.co/biohub/ESMFold2-Fast) ## Usage example @@ -49,8 +44,9 @@ with open("prediction.pdb", "w") as f: ``` `infer_protein` returns the raw outputs (atom coordinates, distogram logits and confidence metrics) as an -[`~models.esmfold2.modeling_esmfold2.ESMFold2Output`] if you need them instead of a PDB string. Generation is -stochastic — set a manual seed for reproducible structures. +[`~models.esmfold2.modeling_esmfold2.ESMFold2Output`] if you need them instead of a PDB string. You may get +slightly different predictions if you run the same sequence multiple times. Set a manual seed if you want exactly +reproducible structures. ## Faster inference with a fused kernel From 206e06e751a06e9b0646f577c2a021f7a16a9972 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 16:47:34 +0100 Subject: [PATCH 51/70] Get rid of CPU-only path that we don't need --- .../models/esmfold2/modeling_esmfold2.py | 17 ++++++------- .../models/esmfold2/test_modeling_esmfold2.py | 25 +++++++++++++------ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index e589ad54b9d6..273433a2c2b1 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -837,27 +837,24 @@ def forward( if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) - # Standard attention with pair bias g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) - logits = torch.einsum("... i h d, ... j h d -> ... i j h", q, k) * self.scale - # ``pair_bias`` is step-invariant; the diffusion sampler precomputes and # caches it across steps. Compute inline when not supplied (e.g. uncached). if pair_bias is None: pair_bias = self.compute_pair_bias(z, bsz, num_diffusion_samples) - logits = logits + pair_bias.to(dtype=logits.dtype) + attn_bias = pair_bias.permute(0, 3, 1, 2) # [B,Q,K,H]->[B,H,Q,K] (H may be 1) if attention_mask is not None: - min_val = torch.finfo(logits.dtype).min - mask_bias = torch.where(attention_mask.bool()[:, None, :, None], 0.0, min_val) - logits = logits + mask_bias.to(dtype=logits.dtype) + min_val = torch.finfo(q.dtype).min + mask_bias = torch.where(attention_mask.bool()[:, None, None, :], 0.0, min_val) + attn_bias = attn_bias + mask_bias + qh, kh, vh = (t.transpose(1, 2) for t in (q, k, v)) # [B,H,Q,D] + ctx = F.scaled_dot_product_attention(qh, kh, vh, attn_mask=attn_bias.to(qh.dtype), scale=self.scale) + ctx = ctx.transpose(1, 2) # [B,H,Q,D]->[B,Q,H,D] - attn = torch.softmax(logits, dim=-2, dtype=torch.float32).to(dtype=v.dtype) - ctx = torch.einsum("... i j h, ... j h d -> ... i h d", attn, v) ctx = g * ctx out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) - out = torch.sigmoid(self.out_gate(s)) * out return out diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index 22eec92cb789..630220265eac 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -43,6 +43,12 @@ from transformers import ESMFold2Model from transformers.models.esmfold2.modeling_esmfold2 import ESMFold2SWA3DRoPEAttention +# TEMP: the public ``biohub/ESMFold2`` snapshot does not yet bundle the ESMC-6B +# backbone under ``esmc.*`` (it loads random → garbage outputs). Point the slow +# integration tests at the locally-bundled checkpoint for now. REVERT to +# "biohub/ESMFold2" once the backbone is bundled there. +_INTEGRATION_CKPT = "Rocketknight1/ESMFold2-merged-temp" + def get_tiny_config(**overrides) -> "ESMFold2Config": """A minimal but internally consistent ESMFold2 config for CPU testing. @@ -202,7 +208,7 @@ class ESMFold2IntegrationTest(TestCasePlus): def test_inference_protein_folding(self): # bf16 is the intended inference regime; the ESMC backbone is bundled in the # checkpoint and loaded with the model. - model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).to(torch_device).eval() + model = ESMFold2Model.from_pretrained(_INTEGRATION_CKPT, dtype=torch.bfloat16).to(torch_device).eval() # Ubiquitin (PDB 1UBQ), a textbook well-folding 76-residue domain. These # diffusion folders draw several samples and the best-ranked is the @@ -237,8 +243,11 @@ def test_inference_deterministic_cpu_fp32(self): # the ESMC-6B backbone + LM projection + pairformer trunk end-to-end. # # Baked from biohub/ESMFold2 @ dd1eae4fb2 (torch 2.11+cu130). A standalone - # multi-sequence version of this check lives in esmfold2_regression.py. - model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.float32).eval() + # multi-sequence version of this check lives in esmfold2_regression.py. The + # distogram is a trunk output (no pair-bias attention); ``ptm`` is fed by + # the diffusion sampler, which now runs the pair-bias attention through + # SDPA on CPU too (~1e-8 vs the old eager path, amplified to ~1e-4 in ptm). + model = ESMFold2Model.from_pretrained(_INTEGRATION_CKPT, dtype=torch.float32).eval() seq = "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG" torch.manual_seed(0) @@ -262,7 +271,7 @@ def test_inference_deterministic_cpu_fp32(self): ] ) torch.testing.assert_close(output["distogram_logits"][0, 0, 1, :8].float(), expected_distogram, rtol=0, atol=0) - self.assertEqual(output["ptm"].max().item(), 0.7425990104675293) + self.assertEqual(output["ptm"].max().item(), 0.7427499890327454) @slow @require_torch_accelerator @@ -293,7 +302,7 @@ def test_inference_deterministic_bf16(self): torch.backends.cudnn.benchmark = False torch.backends.cuda.matmul.allow_tf32 = False - model = ESMFold2Model.from_pretrained("biohub/ESMFold2", dtype=torch.bfloat16).to(torch_device).eval() + model = ESMFold2Model.from_pretrained(_INTEGRATION_CKPT, dtype=torch.bfloat16).to(torch_device).eval() seq = "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG" torch.manual_seed(0) with torch.no_grad(): @@ -302,12 +311,14 @@ def test_inference_deterministic_bf16(self): # TODO(pre-merge): EXACT (atol=0) values are bit-exact only on the machine # they were baked on (RTX 4090, torch 2.11+cu130); bf16 bits differ across # GPUs. Before merging, loosen to atol~0.2 (catches dtype-swap drift of - # ~0.4-2.0 while absorbing cross-GPU variation). + # ~0.4-2.0 while absorbing cross-GPU variation). The diffusion pair-bias + # attention runs through SDPA on GPU, so ``ptm`` (but not the trunk + # distogram) also depends on which SDPA backend the box selects. expected_distogram = torch.tensor([6.46875, 7.71875, 9.4375, 9.3125, 16.125, 18.625, 19.625, 22.625]) torch.testing.assert_close( output["distogram_logits"][0, 0, 1, :8].float().cpu(), expected_distogram, rtol=0, atol=0 ) - self.assertEqual(output["ptm"].max().item(), 0.7421817183494568) + self.assertEqual(output["ptm"].max().item(), 0.7416768074035645) finally: torch.use_deterministic_algorithms(prev[0], warn_only=prev[1]) torch.backends.cudnn.deterministic = prev[2] From 2e4e470a06d43fd96d17a50448fe51e7bebffff3 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 17:09:29 +0100 Subject: [PATCH 52/70] Test cleanup --- .../models/esmfold2/test_modeling_esmfold2.py | 77 +++++-------------- 1 file changed, 19 insertions(+), 58 deletions(-) diff --git a/tests/models/esmfold2/test_modeling_esmfold2.py b/tests/models/esmfold2/test_modeling_esmfold2.py index 630220265eac..b6f8f7d59776 100644 --- a/tests/models/esmfold2/test_modeling_esmfold2.py +++ b/tests/models/esmfold2/test_modeling_esmfold2.py @@ -104,15 +104,22 @@ def get_tiny_config(**overrides) -> "ESMFold2Config": return ESMFold2Config(**kwargs) +class ESMFold2ConfigTester(ConfigTester): + def create_and_test_config_from_and_save_pretrained_composite(self): + # ESMFold2's sub-configs are internal architecture details (no model_type, + # not in the auto mappings), so they can't be reloaded standalone from the + # parent config dir — skip this check. (A no-op rather than SkipTest so the + # remaining run_common_tests checks still run.) + pass + + @require_torch class ESMFold2ConfigTest(unittest.TestCase): def setUp(self): # ESMFold2Config is composite (sub_configs) with no vocab/hidden_size. - self.config_tester = ConfigTester(self, config_class=ESMFold2Config, has_text_modality=False, num_loops=5) - # ESMFold2's sub-configs are internal architecture details (no model_type, - # not in the auto mappings), so the composite "load each sub-config - # standalone from the parent dir" check does not apply. - self.config_tester.create_and_test_config_from_and_save_pretrained_composite = lambda: None + self.config_tester = ESMFold2ConfigTester( + self, config_class=ESMFold2Config, has_text_modality=False, num_loops=5 + ) def test_config(self): self.config_tester.run_common_tests() @@ -233,20 +240,6 @@ def test_inference_protein_folding(self): @slow def test_inference_deterministic_cpu_fp32(self): - # The test above runs in the intended bf16/GPU regime, but GPU bf16 is not - # reproducible run-to-run (matmul nondeterminism amplified through the deep - # trunk swings the distogram logits by O(1)), so it can only assert loose - # confidence floors. CPU/fp32 is bit-exact run-to-run, so here we lock in - # the actual values to a tight tolerance to catch *small* drift from future - # changes. distogram_logits is the cleanest signal: it is computed from the - # trunk with no diffusion sampling, so it is RNG-independent and exercises - # the ESMC-6B backbone + LM projection + pairformer trunk end-to-end. - # - # Baked from biohub/ESMFold2 @ dd1eae4fb2 (torch 2.11+cu130). A standalone - # multi-sequence version of this check lives in esmfold2_regression.py. The - # distogram is a trunk output (no pair-bias attention); ``ptm`` is fed by - # the diffusion sampler, which now runs the pair-bias attention through - # SDPA on CPU too (~1e-8 vs the old eager path, amplified to ~1e-4 in ptm). model = ESMFold2Model.from_pretrained(_INTEGRATION_CKPT, dtype=torch.float32).eval() seq = "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG" @@ -254,41 +247,15 @@ def test_inference_deterministic_cpu_fp32(self): with torch.no_grad(): output = model.infer_protein(seq, num_loops=4, num_diffusion_samples=2, num_sampling_steps=32) - # TODO(pre-merge): EXACT (atol=0) values are bit-exact only on the machine - # they were baked on (RTX 4090, torch 2.11+cu130); fp32 bits can differ across - # CPUs/BLAS. Before merging, loosen to a rounded slice with rtol/atol ~1e-3 - # (e.g. [6.5849, 7.9825, 9.6068, 9.6403, 16.5200, 18.9912, 19.9698, 23.0489]). - expected_distogram = torch.tensor( - [ - 6.584877014160156, - 7.982535362243652, - 9.60677433013916, - 9.640305519104004, - 16.519989013671875, - 18.99123764038086, - 19.96978187561035, - 23.04886817932129, - ] + expected_distogram = torch.tensor([6.5849, 7.9825, 9.6068, 9.6403, 16.5200, 18.9912, 19.9698, 23.0489]) + torch.testing.assert_close( + output["distogram_logits"][0, 0, 1, :8].float(), expected_distogram, rtol=1e-3, atol=1e-3 ) - torch.testing.assert_close(output["distogram_logits"][0, 0, 1, :8].float(), expected_distogram, rtol=0, atol=0) - self.assertEqual(output["ptm"].max().item(), 0.7427499890327454) + self.assertAlmostEqual(output["ptm"].max().item(), 0.7427, delta=1e-2) @slow @require_torch_accelerator def test_inference_deterministic_bf16(self): - # bf16/GPU is the production regime AND the one most exposed to dtype bugs - # (an op run in bf16 where the fork uses fp32, or vice-versa). Such bugs only - # surface in bf16, so we lock this path too. bf16/GPU with default kernels is - # not reproducible run-to-run, but with deterministic algorithms it becomes - # bit-exact (run-to-run AND cross-process), so we can baked-compare it tightly: - # a representative dtype regression (an fp32-pinned norm computing in bf16) - # shifts these distogram values by ~0.4-2.0, far above the deterministic floor. - # - # Baked from biohub/ESMFold2 @ dd1eae4fb2 on an RTX 4090 (torch 2.11+cu130). - # The exact bf16 bits are hardware-specific; the tolerance is loose enough to - # catch dtype drift but a different GPU may need a re-bake. esmfold2_regression.py - # locks all four sequences this way on the local box (CUBLAS_WORKSPACE_CONFIG=:4096:8 - # may be needed on some builds for fully deterministic cuBLAS). prev = ( torch.are_deterministic_algorithms_enabled(), torch.is_deterministic_algorithms_warn_only_enabled(), @@ -308,17 +275,11 @@ def test_inference_deterministic_bf16(self): with torch.no_grad(): output = model.infer_protein(seq, num_loops=4, num_diffusion_samples=2, num_sampling_steps=32) - # TODO(pre-merge): EXACT (atol=0) values are bit-exact only on the machine - # they were baked on (RTX 4090, torch 2.11+cu130); bf16 bits differ across - # GPUs. Before merging, loosen to atol~0.2 (catches dtype-swap drift of - # ~0.4-2.0 while absorbing cross-GPU variation). The diffusion pair-bias - # attention runs through SDPA on GPU, so ``ptm`` (but not the trunk - # distogram) also depends on which SDPA backend the box selects. - expected_distogram = torch.tensor([6.46875, 7.71875, 9.4375, 9.3125, 16.125, 18.625, 19.625, 22.625]) + expected_distogram = torch.tensor([6.47, 7.72, 9.44, 9.31, 16.12, 18.62, 19.62, 22.62]) torch.testing.assert_close( - output["distogram_logits"][0, 0, 1, :8].float().cpu(), expected_distogram, rtol=0, atol=0 + output["distogram_logits"][0, 0, 1, :8].float().cpu(), expected_distogram, rtol=0, atol=0.2 ) - self.assertEqual(output["ptm"].max().item(), 0.7416768074035645) + self.assertAlmostEqual(output["ptm"].max().item(), 0.742, delta=0.05) finally: torch.use_deterministic_algorithms(prev[0], warn_only=prev[1]) torch.backends.cudnn.deterministic = prev[2] From 7c7d8c80e676d52be51aec52e64c26ff213857ac Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 17:49:31 +0100 Subject: [PATCH 53/70] Doc dates --- docs/source/en/model_doc/esmc.md | 2 +- docs/source/en/model_doc/esmfold2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index be1b55c3e6e3..41887ef3bc03 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-15.* +*This model was contributed to Hugging Face Transformers on 2026-06-17.* # ESMC diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 476f70550870..8ac4b4298cfa 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-15.* +*This model was contributed to Hugging Face Transformers on 2026-06-17.* # ESMFold2 From a76c930ef58e324b9d99cf180329bde10e0e4607 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 18 Jun 2026 14:52:38 +0100 Subject: [PATCH 54/70] Comments cleanup --- src/transformers/models/esmc/modeling_esmc.py | 21 ++--- src/transformers/models/esmc/modular_esmc.py | 29 ++----- .../models/esmc/tokenization_esmc.py | 1 - .../models/esmfold2/configuration_esmfold2.py | 8 +- .../models/esmfold2/modeling_esmfold2.py | 84 ++++++------------- .../models/esmfold2/protein_utils.py | 5 +- 6 files changed, 44 insertions(+), 104 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 444413579f43..5cb5da57dec8 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -47,17 +47,13 @@ class ESMCRotaryEmbedding(nn.Module): """Rotary position embeddings (RoPE), returning ``(cos, sin)``. - Identical to :class:`~transformers.models.esm.modeling_esm.EsmRotaryEmbedding` - (``inv_freq = 1 / theta^(arange(0, dim, 2) / dim)`` with ``dim = d_model // n_heads``, - ``cos`` / ``sin`` built in fp32), with two ESMC-specific tweaks: + Two ESMC-specific tweaks over the base implementation: * ``inv_freq`` is a **non-persistent** buffer (recomputed from the config, never stored in the checkpoint). * ``_apply`` is overridden so ``inv_freq`` stays fp32 even when the module is cast - to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16). - ``nn.Module._apply`` would otherwise round the rotary frequencies to bf16, which - drifts the RoPE angles and is the dominant source of bf16 fork-vs-port divergence - in the ESMFold2 backbone. + to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16), so the + rotary frequencies aren't rounded. """ inv_freq: torch.Tensor # fix linting for `register_buffer` @@ -141,14 +137,9 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: class ESMCMLP(nn.Module): - """SwiGLU feed-forward network, reusing :class:`ModernBertMLP`'s gated forward. - - ``ModernBertMLP`` computes ``Wo(act(input) * gate)`` with ``input, gate = - Wi(x).chunk(2, dim=-1)`` — exactly ESMC's SwiGLU once ``act`` is SiLU. ESMC was - trained with ``bias=False`` and no MLP dropout, and rounds the hidden dim to a - multiple of 256. The pre-MLP LayerNorm lives in :class:`ESMCLayer` (``mlp_norm``); - the published checkpoint's ``ffn.fc1_weight`` / ``ffn.fc2_weight`` map onto - ``mlp.Wi`` / ``mlp.Wo`` via ``conversion_mapping.py``. + """SwiGLU feed-forward network reusing :class:`ModernBertMLP`'s gated forward + (``Wo(SiLU(input) * gate)``). Bias-free, no dropout; hidden dim rounded to a + multiple of 256. """ def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index d9516f65bb06..83e1b1036704 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -37,12 +37,8 @@ ) from ..esm.modeling_esm import EsmClassificationHead, EsmRotaryEmbedding, eager_attention_forward -# ESMC applies RoPE in the activation dtype (no fp32 upcast), matching the reference -# implementation's bf16 numerics. Llama's `apply_rotary_pos_emb` is exactly that -# no-upcast variant; `esm`'s upcasts q/k to fp32 and would drift bf16 inference (the -# dominant fork-vs-port divergence on the ESMFold2 backbone, ~0.3 over 80 layers on -# ESMC-6B), so we reuse Llama's rather than esm's. `rotate_half` is pulled in by the -# modular converter as a dependency of the imported function. +# RoPE is applied in the activation dtype (no fp32 upcast); Llama's `apply_rotary_pos_emb` +# is that no-upcast variant. (`rotate_half` is pulled in by the modular converter as a dependency.) from ..llama.modeling_llama import apply_rotary_pos_emb from ..modernbert.modeling_modernbert import ModernBertMLP from .configuration_esmc import ESMCConfig @@ -60,17 +56,13 @@ class ESMCRotaryEmbedding(EsmRotaryEmbedding): """Rotary position embeddings (RoPE), returning ``(cos, sin)``. - Identical to :class:`~transformers.models.esm.modeling_esm.EsmRotaryEmbedding` - (``inv_freq = 1 / theta^(arange(0, dim, 2) / dim)`` with ``dim = d_model // n_heads``, - ``cos`` / ``sin`` built in fp32), with two ESMC-specific tweaks: + Two ESMC-specific tweaks over the base implementation: * ``inv_freq`` is a **non-persistent** buffer (recomputed from the config, never stored in the checkpoint). * ``_apply`` is overridden so ``inv_freq`` stays fp32 even when the module is cast - to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16). - ``nn.Module._apply`` would otherwise round the rotary frequencies to bf16, which - drifts the RoPE angles and is the dominant source of bf16 fork-vs-port divergence - in the ESMFold2 backbone. + to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16), so the + rotary frequencies aren't rounded. """ def __init__(self, config: ESMCConfig, device=None): @@ -96,14 +88,9 @@ def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: class ESMCMLP(ModernBertMLP): - """SwiGLU feed-forward network, reusing :class:`ModernBertMLP`'s gated forward. - - ``ModernBertMLP`` computes ``Wo(act(input) * gate)`` with ``input, gate = - Wi(x).chunk(2, dim=-1)`` — exactly ESMC's SwiGLU once ``act`` is SiLU. ESMC was - trained with ``bias=False`` and no MLP dropout, and rounds the hidden dim to a - multiple of 256. The pre-MLP LayerNorm lives in :class:`ESMCLayer` (``mlp_norm``); - the published checkpoint's ``ffn.fc1_weight`` / ``ffn.fc2_weight`` map onto - ``mlp.Wi`` / ``mlp.Wo`` via ``conversion_mapping.py``. + """SwiGLU feed-forward network reusing :class:`ModernBertMLP`'s gated forward + (``Wo(SiLU(input) * gate)``). Bias-free, no dropout; hidden dim rounded to a + multiple of 256. """ def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index af66b2d771d1..c28ca1a3c425 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -121,7 +121,6 @@ def __init__( eos_token = self._ensure_str(eos_token) chain_break_token = self._ensure_str(chain_break_token) - # A character-level tokenizer is equivalent to BPE with no merges. bpe = BPE(token_to_id, merges=[], unk_token=unk_token) tokenizer = Tokenizer(bpe) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 90b02fa089e3..af2ec821013f 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -88,12 +88,8 @@ class AtomAttentionConfig(PreTrainedConfig): @strict class FoldingTrunkConfig(PreTrainedConfig): - """Config for a pairwise folding trunk stack. - - The trunk's ``ESMFold2PairUpdateBlock`` is attention-free (triangle - multiplication + transition), so only ``num_hidden_layers`` is read; the - pair dim comes from the parent's ``d_pair``. - """ + """Config for a pairwise folding trunk stack (only ``num_hidden_layers`` is read; + pair dim comes from the parent's ``d_pair``).""" attribute_map = { "n_layers": "num_hidden_layers", diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 273433a2c2b1..7d61bc0130d4 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -46,12 +46,8 @@ class ESMFold2LayerNorm(nn.LayerNorm): """LayerNorm that always computes in fp32, with its weight stored at the model dtype. - ESMFold2 runs in the loaded dtype (bf16 for inference) but its LayerNorms must match - the reference's fp32 norm *compute* (the fork upcasts via autocast). Keeping the weight - at the model dtype means ``from_pretrained(dtype=bf16)`` rounds it to bf16 exactly like - the reference's bf16-trained norm weights, while this ``forward`` upcasts to fp32 for - the computation and casts the result back — so no post-load weight fix-up is needed - (an fp32 load keeps full-precision weights and an fp32 compute, both no-ops here). + A bf16 load rounds the weight to bf16, but the norm itself still computes in fp32 + (upcast here, cast back); an fp32 load is a no-op. """ def forward(self, x: Tensor) -> Tensor: @@ -103,8 +99,8 @@ def __init__(self, d_model: int, d_cond: int, eps: float = 1e-5) -> None: def forward(self, a: Tensor, s: Tensor) -> Tensor: a_norm = F.layer_norm(a.float(), (self.d_model,), None, None, self.eps) s_norm = F.layer_norm(s.float(), (self.d_cond,), self.s_scale.float(), None, self.eps).to(s.dtype) - # gate/shift in bf16 (matches the reference's autocast); a_norm is fp32 so the - # affine promotes to fp32, then downcast for the next op. + # gate/shift in bf16; a_norm is fp32 so the affine promotes to fp32, then + # downcast for the next op. gate = torch.sigmoid(self.s_gate(s_norm)) shift = self.s_shift(s_norm) return (gate * a_norm + shift).to(a.dtype) @@ -128,8 +124,8 @@ def __init__(self, c: int) -> None: self.register_buffer("b", torch.randn(c)) def forward(self, t_hat: Tensor) -> Tensor: - # w/b are kept fp32 (ESMFold2Model._keep_in_fp32_modules_strict), so the random - # frequencies/phases — and the cos embedding — are computed at full precision. + # w/b are kept fp32 (see ESMFold2Model._keep_in_fp32_modules_strict) so the cos + # embedding is full precision. t = torch.as_tensor(t_hat, device=self.w.device, dtype=self.w.dtype).reshape(-1) return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) @@ -1390,8 +1386,7 @@ def sample( inference_cache=inference_cache, ) - # Reverse diffusion alignment (Kabsch). _weighted_rigid_align upcasts - # to fp32 internally for the SVD/det. + # Reverse diffusion alignment (Kabsch). x_noisy = self._weighted_rigid_align(x_noisy.float(), x_denoised.float(), atom_mask, atom_mask) x_noisy = x_noisy.to(dtype=x_denoised.dtype) @@ -1636,11 +1631,6 @@ def forward(self, hidden_states: Tensor) -> Tensor: _EPS = 1e-5 -# Default for the triangle / OPM / pair-transition L² ops. Caps peak memory -# so L≈2k folds on an 80 GB GPU (~76 GB peak at chunk=128 for L=1438; -# chunk=64 leaves headroom for the largest foldbench targets). Override via -# ``model.set_chunk_size(...)``; pass None to disable chunking (faster for -# short L but OOM-prone past ~600). _DEFAULT_CHUNK_SIZE = 64 @@ -1651,14 +1641,9 @@ def forward(self, hidden_states: Tensor) -> Tensor: class ESMFold2TriangleMultiplicativeBlock(nn.Module): """Triangle multiplicative update block with gated signal routing. - The O(N^3) triangular contraction below is the trunk's dominant cost. Loading - with ``ESMFold2Model.from_pretrained(..., device_map="cuda", use_kernels=True)`` - (CUDA + inference) swaps the whole block forward for a fused Triton kernel from - the Hub (see the ``hub_kernels`` mapping); the pure-PyTorch ``forward`` here stays - as the reference/fallback. The kernel reads this module's parameters - (``norm_start``/``norm_mix``/``proj_bundle``/``proj_emit``/``proj_gate``) and - matches ``forward``'s ``(pair_grid, visibility)`` signature, returning the - residual-free delta. + The O(N^3) contraction is the trunk's dominant cost; ``use_kernels=True`` (CUDA + + inference) swaps this whole forward for a fused Triton Hub kernel matching the + ``(pair_grid, visibility)`` signature and returning the residual-free delta. """ _FLOW_TO_EINSUM = {"outgoing": "bikd,bjkd->bijd", "incoming": "bkid,bkjd->bijd"} @@ -1679,9 +1664,7 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False) - # Default chunked for memory on long sequences; tests override with - # ``set_chunk_size(None)`` for the unchunked path under bit-exact bf16 - # parity checks. + # Default chunked for memory on long sequences (set_chunk_size(None) disables). self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE def set_chunk_size(self, chunk_size: int | None) -> None: @@ -1710,12 +1693,8 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor normalized_grid = self.norm_start(pair_grid) bundled = self.proj_bundle(normalized_grid) signal, gate_logits = bundled.split(2 * self.latent_channels, dim=-1) - # Gates and the O(N^3) contraction run in the activation dtype (bf16). This - # matches the reference: under its autocast the einsum is downcast to bf16, - # and the fused Triton kernel likewise contracts in bf16 — the dtype the - # checkpoint was trained with. Keeping these in fp32 was a (marginal) - # precision *up* that diverges from training and is slower on the trunk's - # dominant op. ``norm_start``/``norm_mix`` stay fp32. A no-op in fp32. + # Gates and the O(N^3) contraction run in the activation dtype (bf16, the + # training dtype); ``norm_start``/``norm_mix`` stay fp32. routed = signal * torch.sigmoid(gate_logits) routed = routed * visibility.unsqueeze(-1) @@ -1756,7 +1735,7 @@ def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: super().__init__() self.norm = ESMFold2LayerNorm(d_model) self.ffn = ESMFold2SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) - # Default chunked; set_chunk_size(None) disables for bit-exact parity tests. + # Default chunked; set_chunk_size(None) disables. self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE def set_chunk_size(self, chunk_size: int | None) -> None: @@ -1788,7 +1767,7 @@ def set_chunk_size(self, chunk_size: int | None) -> None: self.pair_transition.set_chunk_size(chunk_size) def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: - # HF model is inference-only, so the trained row-shared dropout (r=0) is a no-op. + # Inference-only: trained row-shared dropout omitted. pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) pair = self.pair_transition(pair) @@ -2272,11 +2251,9 @@ class ESMFold2PreTrainedModel(PreTrainedModel): _supports_attention_backend = True def _init_weights(self, module): - # The base initializer handles Linear/Embedding/LayerNorm; below we (re)apply the - # few non-default inits the architecture needs (adaLN-Zero gates, identity/recurrent - # parcae params, zeroed projections). These live here, not in submodule __init__s, - # so they survive `post_init()` and are not wastefully run before weight loading. - # The `init.*` helpers are load-flag aware (they no-op on already-loaded weights). + # Re-apply the few non-default inits (adaLN-Zero gates, parcae identity/recurrence, + # zeroed projections). Kept here (not in submodule __init__s) so they survive + # post_init(); the load-flag-aware init.* helpers no-op on loaded weights. super()._init_weights(module) if isinstance(module, ESMFold2Model): init.eye_(module.parcae_readout.weight) @@ -2463,12 +2440,9 @@ def __init__(self, config: ESMFold2Config) -> None: self.language_model = ESMFold2LanguageModelShim( d_z=d_pair, d_model=config.esmc_config.d_model, num_layers=config.esmc_config.n_layers ) - # The ESMC backbone is a bundled submodule: built here with random weights - # (instant, no I/O), then populated by the parent's from_pretrained like any - # other composite sub-model (its weights live under ``esmc.*`` in the ESMFold2 - # checkpoint). It is frozen in effect — ``forward`` detaches its hidden states - # before they enter the trunk, so no gradient ever reaches it — and the bf16 - # autocast in ``_compute_lm_hidden_states`` reproduces the LM precision regime. + # ESMC backbone built here with random weights (no I/O), then populated by + # from_pretrained from the checkpoint's ``esmc.*`` weights. Frozen in effect: + # forward detaches its hidden states before they enter the trunk. self.esmc = AutoModel.from_config(config.esmc_config) pf = config.folding_trunk @@ -2517,12 +2491,8 @@ def _compute_lm_hidden_states( mol_type: Tensor, tok_mask: Tensor, ) -> Tensor: - # Run the frozen ESMC backbone exactly as the reference does: plain weights - # under a bf16 autocast (the fork's ``_lm_precision_context``). For a bf16 - # backbone this puts norms/softmax in fp32 and matmuls/rotary in bf16, - # reproducing the checkpoint's training regime bit-for-bit; disabled (a - # no-op) for an fp32 backbone. This autocast is scoped to the backbone only - # — the trunk stays dtype-honest. + # bf16 autocast scoped to the ESMC backbone (norms/softmax fp32, matmuls/rotary + # bf16); a no-op for an fp32 backbone, and the trunk stays dtype-honest. use_amp = next(self.esmc.parameters()).dtype == torch.bfloat16 with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=use_amp): return compute_lm_hidden_states( @@ -2558,10 +2528,9 @@ def _run_one_loop( b_mat: Tensor, total_steps: int, ) -> Tensor: - # Helper method (not inline) so per-iter locals free on return — - # otherwise leaks ~2 GB L²×c_z into distogram/sample scope. - # training=True forces dropout under eval(), matching the per-loop - # dropout strategy used at train time. + # Helper method (not inline) so per-iter L²×c_z locals free on return (else + # ~2 GB leaks into distogram/sample scope). training=True forces the per-loop + # dropout under eval(). lm_cfg = self.config.lm_encoder _per_loop_lm_dropout = ( lm_z is not None @@ -2737,7 +2706,6 @@ def forward( "msa_attention_mask": msa_attn, } - # Method call (not inline loop) frees per-iter L²×c_z locals. z = self._run_one_loop( z=z, z_init=z_init, diff --git a/src/transformers/models/esmfold2/protein_utils.py b/src/transformers/models/esmfold2/protein_utils.py index 09285e6d8ed8..23d1fbd470cc 100644 --- a/src/transformers/models/esmfold2/protein_utils.py +++ b/src/transformers/models/esmfold2/protein_utils.py @@ -352,8 +352,7 @@ }, } -# Protonated nitrogens at physiological pH (matches CHARGED_ATOMS in the -# opensource constants for the protein subset). +# Protonated nitrogens at physiological pH. PROTEIN_CHARGED_ATOMS: dict[tuple[str, str], int] = { ("LYS", "NZ"): 1, ("ARG", "NH2"): 1, @@ -400,7 +399,7 @@ def prepare_protein_features(sequence: str) -> dict[str, Tensor]: token_atom_starts.append(atom_cursor) for name in atom_names: charge = PROTEIN_CHARGED_ATOMS.get((res_3, name), 0) - element = name[0] # protein heavy atoms are all single-letter C/N/O/S + element = name[0] ref_pos = PROTEIN_REF_POS[res_3][name] atom_records.append((t_idx, name, element, charge, ref_pos)) atom_cursor += 1 From 3579775144f2fffa21777fe009d01261e4e0acb8 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 22 Jun 2026 18:16:26 +0100 Subject: [PATCH 55/70] Re-add the kernel after rebase --- src/transformers/integrations/hub_kernels.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/transformers/integrations/hub_kernels.py b/src/transformers/integrations/hub_kernels.py index 41c79bd6da86..e9cf6a1938ca 100644 --- a/src/transformers/integrations/hub_kernels.py +++ b/src/transformers/integrations/hub_kernels.py @@ -134,6 +134,15 @@ def _build_kernel_mapping() -> dict: trust_remote_code=True, ), }, + "ESMFold2TriangleMultiplication": { + "cuda": { + Mode.INFERENCE: LayerRepository( + repo_id="Rocketknight1/esmfold2-trimul-kernel", + layer_name="ESMFold2TriangleMultiplication", + version=1, + ), + }, + }, "SwiGLUMLP": { "cuda": { Mode.INFERENCE | Mode.TORCH_COMPILE: LayerRepository( From 8a2e3bc075f4190dbd48df6b3617872c7fb1911b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 22 Jun 2026 18:20:29 +0100 Subject: [PATCH 56/70] Fix dates --- docs/source/en/model_doc/esmc.md | 2 +- docs/source/en/model_doc/esmfold2.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 41887ef3bc03..328a3946c727 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-17.* +*This model was contributed to Hugging Face Transformers on 2026-06-22.* # ESMC diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 8ac4b4298cfa..ba795cc99dc6 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-17.* +*This model was contributed to Hugging Face Transformers on 2026-06-22.* # ESMFold2 From 6667c241595f279e35c94c398d96d0eafa925c06 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jun 2026 15:57:24 +0100 Subject: [PATCH 57/70] Push ESMC fixes --- docs/source/en/model_doc/esmc.md | 19 +- docs/source/en/model_doc/esmfold2.md | 2 +- src/transformers/conversion_mapping.py | 53 +- src/transformers/models/esmc/__init__.py | 10 +- .../models/esmc/configuration_esmc.py | 34 +- src/transformers/models/esmc/modeling_esmc.py | 483 +++++------------- src/transformers/models/esmc/modular_esmc.py | 464 +++++------------ .../models/esmc/tokenization_esmc.py | 35 +- 8 files changed, 344 insertions(+), 756 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 328a3946c727..d46117992024 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-22.* +*This model was contributed to Hugging Face Transformers on 2026-06-25.* # ESMC @@ -26,25 +26,28 @@ Like [ESM-2](./esm), ESMC produces per-residue representations that are useful f ESMC is suitable for fine-tuning on protein classification or token classification tasks. It is also used as the backbone of [ESMFold2](./esmfold2), where it generates representations that are used as input to the folding head. -Pre-trained checkpoints are available on the Hugging Face Hub, including -[`biohub/ESMC-300M`](https://huggingface.co/biohub/ESMC-300M), -[`biohub/ESMC-600M`](https://huggingface.co/biohub/ESMC-600M) and -[`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B). +Pre-trained checkpoints are available on the Hugging Face Hub: + +- [`biohub/ESMC-300M`](https://huggingface.co/biohub/ESMC-300M) +- [`biohub/ESMC-600M`](https://huggingface.co/biohub/ESMC-600M) +- [`biohub/ESMC-6B`](https://huggingface.co/biohub/ESMC-6B) ## Usage example ```python import torch -from transformers import AutoTokenizer, ESMCModel +from transformers import AutoModel, AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") -model = ESMCModel.from_pretrained("biohub/ESMC-300M") +# ESMC is registered with the auto classes (AutoModel, AutoModelForMaskedLM, +# AutoModelForSequenceClassification, AutoModelForTokenClassification). +model = AutoModel.from_pretrained("biohub/ESMC-300M") inputs = tokenizer("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ", return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) -# Per-residue representations of shape (batch, sequence_length, d_model). +# Per-residue representations of shape (batch, sequence_length, hidden_size). representations = outputs.last_hidden_state ``` diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index ba795cc99dc6..403bb23e0fe4 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-22.* +*This model was contributed to Hugging Face Transformers on 2026-06-25.* # ESMFold2 diff --git a/src/transformers/conversion_mapping.py b/src/transformers/conversion_mapping.py index 044e65e18a3f..8d666d5fa20f 100755 --- a/src/transformers/conversion_mapping.py +++ b/src/transformers/conversion_mapping.py @@ -790,29 +790,38 @@ def _build_checkpoint_conversion_mapping(): "rotary_embeddings.inv_freq", ), ], - # The published ESMC checkpoints fuse each transformer block's two pre-norms - # into the QKV / FFN projections: ``attn.layernorm_qkv`` is ``LayerNorm + Linear`` - # and ``ffn`` is ``LayerNorm + SwiGLU`` (weights ``fc1_weight`` / ``fc2_weight``). - # The model uses the standard ModernBERT-style pre-norm layout instead -- separate - # block-level ``attn_norm`` / ``mlp_norm`` with fused ``attn.Wqkv`` / ``attn.Wo`` - # and ``mlp.Wi`` / ``mlp.Wo``. Remap on load (and reverse on save). Registered on - # the "esmc" model_type so it also covers the bundled ESMC backbone nested under - # ``esmc.*`` inside an ESMFold2 checkpoint (sub-modules are remapped by model_type). "esmc": [ - WeightRenaming(r"attn\.layernorm_qkv\.layer_norm_weight", "attn_norm.weight"), - WeightRenaming(r"attn\.layernorm_qkv\.layer_norm_bias", "attn_norm.bias"), - WeightRenaming(r"attn\.layernorm_qkv\.weight", "attn.Wqkv.weight"), - WeightRenaming(r"attn\.out_proj\.", "attn.Wo."), - WeightRenaming(r"ffn\.layer_norm_weight", "mlp_norm.weight"), - WeightRenaming(r"ffn\.layer_norm_bias", "mlp_norm.bias"), - WeightRenaming(r"ffn\.fc1_weight", "mlp.Wi.weight"), - WeightRenaming(r"ffn\.fc2_weight", "mlp.Wo.weight"), - ], - # Scoped to the class (not the "esmc" model_type) so it only applies to the - # head model: the published ESMC checkpoints store the masked-LM head as an - # ``nn.Sequential`` (keys ``lm_head.{0,2,3}``); ``ESMCForMaskedLM`` uses a - # named ``ESMCMaskedLMHead`` (``dense`` / ``layer_norm`` / ``decoder``). Remap - # on load (and reverse on save). ``ESMCModel`` (the backbone) has no lm_head. + WeightRenaming(r"transformer\.blocks", "layers"), + # The negative lookbehinds anchor the *reverse* search to the final encoder + # norm only (they are stripped from the forward replacement), so saving does + # not rewrite the "norm" inside ``input_layernorm`` / ``post_attention_layernorm``. + WeightRenaming(r"transformer\.norm\.", r"(?"``), used for masked language modelling. classifier_dropout (`float`, *optional*, defaults to 0.1): Dropout ratio for the classification head. - rope_theta (`float`, *optional*, defaults to 10000.0): - The base period of the rotary position embeddings (RoPE). + max_position_embeddings (`int`, *optional*, defaults to 2048): + Nominal maximum sequence length. RoPE imposes no hard limit, so this only sizes + the rotary cache for dynamic RoPE variants (unused by the default RoPE). + rope_parameters (`RopeParameters` or `dict`, *optional*): + Dictionary configuring the rotary position embeddings (RoPE). When omitted, the + default RoPE is used with `rope_theta` = `default_theta` (10000.0). See + [`~modeling_rope_utils.RopeParameters`] for the accepted keys. expansion_ratio (`float`, *optional*, defaults to `8/3`): - Hidden-dim expansion ratio for the SwiGLU feed-forward network (the hidden - size is rounded up to a multiple of 256). + Hidden-dim expansion ratio for the SwiGLU feed-forward network. When + `intermediate_size` is not given it is derived from this as + `expansion_ratio * hidden_size` rounded up to a multiple of 256. + intermediate_size (`int`, *optional*): + Dimensionality of the SwiGLU feed-forward layer. Defaults to the value + derived from `expansion_ratio` (see above). + hidden_act (`str`, *optional*, defaults to `"silu"`): + The non-linear activation function in the feed-forward network. qk_layernorm (`bool`, *optional*, defaults to `True`): Whether to apply LayerNorm to queries and keys before computing attention. scale_residue (`bool`, *optional*, defaults to `True`): @@ -61,6 +73,7 @@ class ESMCConfig(PreTrainedConfig): """ model_type = "esmc" + default_theta = 10000.0 attribute_map = { "d_model": "hidden_size", "n_heads": "num_attention_heads", @@ -75,11 +88,20 @@ class ESMCConfig(PreTrainedConfig): mask_token_id: int | None = 32 initializer_range: float | None = 0.02 classifier_dropout: float | None = 0.1 - rope_theta: float | None = 10000.0 + max_position_embeddings: int | None = 2048 + rope_parameters: RopeParameters | dict | None = None expansion_ratio: float | None = 8 / 3 + intermediate_size: int | None = None + hidden_act: str | None = "silu" + mlp_bias: bool | None = False qk_layernorm: bool | None = True scale_residue: bool | None = True tie_word_embeddings: bool | None = False + def __post_init__(self, **kwargs): + super().__post_init__(**kwargs) + if self.intermediate_size is None: + self.intermediate_size = int(((self.expansion_ratio * self.hidden_size) + 255) // 256 * 256) + __all__ = ["ESMCConfig"] diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 5cb5da57dec8..da5991a722bd 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -27,48 +27,37 @@ from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss from torch.nn import functional as F -from ... import initialization as init +from ...activations import ACT2FN from ...integrations import use_kernel_func_from_hub -from ...masking_utils import create_bidirectional_mask # type: ignore[import] -from ...modeling_outputs import ( # type: ignore[import] - BaseModelOutput, - MaskedLMOutput, - SequenceClassifierOutput, - TokenClassifierOutput, -) -from ...modeling_rope_utils import dynamic_rope_update -from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel # type: ignore[import] +from ...masking_utils import create_bidirectional_mask, packed_sequence_mask_function +from ...modeling_layers import GradientCheckpointingLayer +from ...modeling_outputs import BaseModelOutput, MaskedLMOutput, SequenceClassifierOutput, TokenClassifierOutput +from ...modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack -from ...utils import TransformersKwargs, auto_docstring, can_return_tuple # type: ignore[import] -from ...utils.generic import maybe_autocast +from ...utils import TransformersKwargs, auto_docstring, can_return_tuple +from ...utils.generic import is_flash_attention_requested, maybe_autocast from .configuration_esmc import ESMCConfig class ESMCRotaryEmbedding(nn.Module): - """Rotary position embeddings (RoPE), returning ``(cos, sin)``. - - Two ESMC-specific tweaks over the base implementation: - - * ``inv_freq`` is a **non-persistent** buffer (recomputed from the config, never - stored in the checkpoint). - * ``_apply`` is overridden so ``inv_freq`` stays fp32 even when the module is cast - to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16), so the - rotary frequencies aren't rounded. - """ - inv_freq: torch.Tensor # fix linting for `register_buffer` def __init__(self, config: ESMCConfig, device=None): super().__init__() + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings self.config = config - self.rope_type = {} - curr_inv_freq, curr_attention_scaling = self.compute_default_rope_parameters(self.config, device) - self.register_buffer("inv_freq", curr_inv_freq) - setattr(self, "attention_scaling", curr_attention_scaling) - inv_freq, _ = self.compute_default_rope_parameters(config, device) + self.rope_type = self.config.rope_parameters["rope_type"] + rope_init_fn: Callable = self.compute_default_rope_parameters + if self.rope_type != "default": + rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + inv_freq, self.attention_scaling = rope_init_fn(self.config, device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + self.register_buffer("original_inv_freq", inv_freq.clone(), persistent=False) @staticmethod def compute_default_rope_parameters( @@ -85,12 +74,11 @@ def compute_default_rope_parameters( The device to use for initialization of the inverse frequencies. seq_len (`int`, *optional*): The current sequence length. Unused for this type of RoPE. - Returns: Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE). """ - base = config.rope_theta + base = config.rope_parameters["rope_theta"] dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads attention_factor = 1.0 # Unused in this type of RoPE @@ -103,56 +91,41 @@ def compute_default_rope_parameters( @torch.no_grad() @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) - def forward(self, x, position_ids, layer_type=None): - inv_freq = getattr(self, "inv_freq") - attention_scaling = getattr(self, "attention_scaling") - - inv_freq_expanded = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) + def forward(self, x, position_ids): + inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) position_ids_expanded = position_ids[:, None, :].float() device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" with maybe_autocast(device_type=device_type, enabled=False): # Force float32 freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) emb = torch.cat((freqs, freqs), dim=-1) - cos = emb.cos() * attention_scaling - sin = emb.sin() * attention_scaling + cos = emb.cos() * self.attention_scaling + sin = emb.sin() * self.attention_scaling return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) def _apply(self, fn, recurse=True): + # Little bit of hackery compared to Llama to match the original's dtypes result = super()._apply(fn, recurse=recurse) inv_freq, _ = self.compute_default_rope_parameters(self.config, device=self.inv_freq.device) self.register_buffer("inv_freq", inv_freq, persistent=False) return result -# --------------------------------------------------------------------------- -# Feed-forward network helpers -# --------------------------------------------------------------------------- - - -def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: - """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" - return int(((expansion_ratio * d_model) + 255) // 256 * 256) - - class ESMCMLP(nn.Module): - """SwiGLU feed-forward network reusing :class:`ModernBertMLP`'s gated forward - (``Wo(SiLU(input) * gate)``). Bias-free, no dropout; hidden dim rounded to a - multiple of 256. - """ - - def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: + def __init__(self, config): super().__init__() - ffn_hidden_size = _swiglu_hidden_dim(expansion_ratio, d_model) - self.Wi = nn.Linear(d_model, 2 * ffn_hidden_size, bias=False) - self.act = nn.SiLU() - self.drop = nn.Identity() - self.Wo = nn.Linear(ffn_hidden_size, d_model, bias=False) + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias) + self.act_fn = ACT2FN[config.hidden_act] - def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: - input, gate = self.Wi(hidden_states).chunk(2, dim=-1) - return self.Wo(self.drop(self.act(input) * gate)) + def forward(self, x): + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + return down_proj def rotate_half(x): @@ -216,47 +189,23 @@ def eager_attention_forward( return attn_output, attn_weights -# --------------------------------------------------------------------------- -# Attention -# --------------------------------------------------------------------------- - - class ESMCAttention(nn.Module): - """Multi-head self-attention with QK LayerNorm and RoPE. - - All dims/flags are read from ``config`` (``d_model``/``n_heads``/ - ``qk_layernorm``). ESMC is bias-free, so the linears carry no bias. - """ - def __init__(self, config: ESMCConfig): super().__init__() self.config = config - d_model = config.d_model - n_heads = config.n_heads - self.d_model = d_model - self.n_heads = n_heads - self.d_head = d_model // n_heads - self.scaling = self.d_head**-0.5 + self.head_dim = config.hidden_size // config.num_attention_heads + self.num_attention_heads = config.num_attention_heads + self.scaling = self.head_dim**-0.5 self.attention_dropout = 0.0 - # ESMC is a bidirectional encoder: never apply causal masking. Without - # this, the sdpa/flash interfaces default `is_causal` to True when no - # attention_mask is passed (unpadded inputs), silently masking causally. self.is_causal = False - # Fused QKV projection (``Wqkv``) and output projection (``Wo``), matching - # ModernBERT's attention layout. The pre-attention LayerNorm lives in - # :class:`ESMCLayer` (``attn_norm``); the published checkpoint's fused - # ``attn.layernorm_qkv`` / ``attn.out_proj`` map onto these via - # ``conversion_mapping.py``. - self.Wqkv = nn.Linear(d_model, d_model * 3, bias=False) - self.Wo = nn.Linear(d_model, d_model, bias=False) - - if config.qk_layernorm: - self.q_ln = nn.LayerNorm(d_model, bias=False) - self.k_ln = nn.LayerNorm(d_model, bias=False) - else: - self.q_ln = nn.Identity() - self.k_ln = nn.Identity() + self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) + self.k_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) + self.v_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) + self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False) + + self.q_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() + self.k_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() def forward( self, @@ -264,32 +213,29 @@ def forward( attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, output_attentions: bool = False, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: """Return ``(context, attn_weights)``. Attention is computed by the backend selected through - ``config._attn_implementation`` (``eager`` / ``sdpa`` / - ``flash_attention_2`` / ...). QK-LayerNorm and RoPE (via - ``position_embeddings``) are applied here; the backend only computes - ``softmax(QKᵀ)V``. ``attn_weights`` is ``None`` unless the backend - exposes the probabilities — ``output_attentions=True`` forces the - ``eager`` interface so they are observable. + ``config._attn_implementation``. ``attn_weights`` is ``None`` unless the backend + exposes the probabilities -- ``output_attentions=True`` forces the ``eager`` + interface so they are observable. """ - b, s, _ = hidden_states.shape - qkv = self.Wqkv(hidden_states) - q, k, v = torch.chunk(qkv, 3, dim=-1) - q = self.q_ln(q).to(q.dtype) - k = self.k_ln(k).to(q.dtype) + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_ln(self.q_proj(hidden_states)).to(hidden_states.dtype) + key_states = self.k_ln(self.k_proj(hidden_states)).to(hidden_states.dtype) + value_states = self.v_proj(hidden_states) - # (B, S, D) -> (B, H, S, Dh) - q = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - k = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - v = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + query_states = query_states.view(hidden_shape).transpose(1, 2) + key_states = key_states.view(hidden_shape).transpose(1, 2) + value_states = value_states.view(hidden_shape).transpose(1, 2) if position_embeddings is not None: cos, sin = position_embeddings - q, k = apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1) + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) attention_interface: Callable = eager_attention_forward if not output_attentions: @@ -299,182 +245,74 @@ def forward( attn_output, attn_weights = attention_interface( self, - q, - k, - v, + query_states, + key_states, + value_states, attention_mask, dropout=0.0 if not self.training else self.attention_dropout, scaling=self.scaling, **kwargs, ) - attn_output = attn_output.reshape(b, s, -1) - return self.Wo(attn_output), attn_weights + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + return self.o_proj(attn_output), attn_weights -# --------------------------------------------------------------------------- -# Transformer blocks -# --------------------------------------------------------------------------- - - -class ESMCLayer(nn.Module): - """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. - - All dims/flags are read from ``config``. The ESM3 residue-scaling factor - (``sqrt(num_hidden_layers / 36)`` when ``config.scale_residue``) is derived - from the config here rather than threaded in. - """ +class ESMCLayer(GradientCheckpointingLayer): + """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling.""" def __init__(self, config: ESMCConfig): super().__init__() - d_model = config.d_model - # Pre-norm layout (cf. ModernBERT): the LayerNorms that the published ESMC - # checkpoint fuses into ``attn.layernorm_qkv`` / ``ffn`` live here as - # ``attn_norm`` / ``mlp_norm`` (remapped in ``conversion_mapping.py``). - self.attn_norm = nn.LayerNorm(d_model) - self.attn = ESMCAttention(config) - self.mlp_norm = nn.LayerNorm(d_model) - self.mlp = ESMCMLP(d_model, config.expansion_ratio) - - self.scaling_factor = math.sqrt(config.n_layers / 36) if config.scale_residue else 1.0 + self.input_layernorm = nn.LayerNorm(config.hidden_size) + self.self_attn = ESMCAttention(config) + self.post_attention_layernorm = nn.LayerNorm(config.hidden_size) + self.mlp = ESMCMLP(config) + self.scaling_factor = math.sqrt(config.num_hidden_layers / 36) if config.scale_residue else 1.0 def forward( self, - x: torch.Tensor, + hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, output_attentions: bool = False, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: - """ - Args: - x: ``(batch, seq_len, d_model)`` - attention_mask: Additive attention bias broadcastable to - ``(batch, num_heads, seq_len, seq_len)``, or ``None`` for full - (unmasked) attention. - output_attentions: When ``True``, returns the per-head attention - weights for this block alongside the residual output. - - Returns: - ``(output, attn_weights_or_None)``. Shape of ``output`` is - ``(batch, seq_len, d_model)``; ``attn_weights`` shape is - ``(batch, num_heads, seq_len, seq_len)`` or ``None``. - """ - attn_out, attn_weights = self.attn( - self.attn_norm(x), + residual = hidden_states + attn_output, attn_weights = self.self_attn( + self.input_layernorm(hidden_states), attention_mask, position_embeddings=position_embeddings, output_attentions=output_attentions, **kwargs, ) - x = x + attn_out / self.scaling_factor - x = x + self.mlp(self.mlp_norm(x)) / self.scaling_factor - return x, attn_weights - - -class ESMCEncoder(nn.Module): - """Stack of :class:`ESMCLayer` layers with a final LayerNorm. All dims/flags - are read from ``config``.""" - - def __init__(self, config: ESMCConfig): - super().__init__() - self.blocks = nn.ModuleList([ESMCLayer(config) for _ in range(config.n_layers)]) - self.norm = nn.LayerNorm(config.d_model, bias=False) - - def forward( - self, - x: torch.Tensor, - attention_mask: torch.Tensor | None = None, - position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, - layers_to_collect: list[int] | None = None, - output_attentions: bool = False, - **kwargs, - ) -> tuple[ - torch.Tensor, - torch.Tensor, - tuple[torch.Tensor, ...], - tuple[torch.Tensor, ...] | None, - ]: - """Run the full transformer stack. - - Args: - x: ``(batch, seq_len, d_model)`` - attention_mask: Additive attention bias forwarded to each block, or - ``None`` for full attention. - layers_to_collect: Layer indices (0-based pre-block inputs plus - ``n_layers`` for the post-norm output) whose hidden states - should be returned. - output_attentions: When ``True``, collects the per-block attention - weights and returns them as the fourth tuple element. - - Returns: - ``(post_norm, pre_norm, hidden_states, attentions)`` where - ``hidden_states`` is a (possibly empty) tuple of tensors and - ``attentions`` is a tuple of per-block ``(B, H, L, L)`` tensors - or ``None`` when ``output_attentions`` is ``False``. - """ - if layers_to_collect is None: - layers_to_collect = [] - - collected: list[torch.Tensor] = [] - all_attentions: list[torch.Tensor] = [] - for layer_idx, block in enumerate(self.blocks): - if layer_idx in layers_to_collect: - collected.append(x) - x, attn_weights = block( - x, - attention_mask, - position_embeddings=position_embeddings, - output_attentions=output_attentions, - **kwargs, - ) - if output_attentions and attn_weights is not None: - all_attentions.append(attn_weights) - - norm_x = self.norm(x) - if len(self.blocks) in layers_to_collect: - collected.append(norm_x) - - attentions = tuple(all_attentions) if output_attentions else None - return norm_x, x, tuple(collected), attentions - - -# --------------------------------------------------------------------------- -# Pre-trained model base class -# --------------------------------------------------------------------------- + # ESM3 residue scaling on each residual branch. + hidden_states = residual + attn_output / self.scaling_factor + residual = hidden_states + hidden_states = residual + self.mlp(self.post_attention_layernorm(hidden_states)) / self.scaling_factor + return hidden_states, attn_weights @auto_docstring class ESMCPreTrainedModel(PreTrainedModel): config_class = ESMCConfig base_model_prefix = "esmc" - supports_gradient_checkpointing = False + supports_gradient_checkpointing = True _supports_sdpa = True _supports_flash_attn = True _supports_attention_backend = True _no_split_modules = ["ESMCLayer"] - _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] - - def _init_weights(self, module: nn.Module): - if isinstance(module, ESMCRotaryEmbedding): - inv_freq, _ = module.compute_default_rope_parameters(module.config) - init.copy_(module.inv_freq, inv_freq) - else: - # nn.Linear / nn.Embedding / nn.LayerNorm via the base initializer. - super()._init_weights(module) - - -# --------------------------------------------------------------------------- -# Base encoder model -# --------------------------------------------------------------------------- + # ``inv_freq`` / ``original_inv_freq`` are non-persistent rotary buffers; ``_extra_state`` + # keys come from the published checkpoint's fused TransformerEngine layout. + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$", r"\.inv_freq$", r"\.original_inv_freq$"] @auto_docstring class ESMCModel(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) - self.embed = nn.Embedding(config.vocab_size, config.d_model) + self.embed = nn.Embedding(config.vocab_size, config.hidden_size) self.rotary_emb = ESMCRotaryEmbedding(config) - self.transformer = ESMCEncoder(config) + self.layers = nn.ModuleList([ESMCLayer(config) for _ in range(config.num_hidden_layers)]) + self.norm = nn.LayerNorm(config.hidden_size, bias=False) self.post_init() def get_input_embeddings(self) -> nn.Embedding: @@ -493,16 +331,18 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor for chain-aware attention masking. Tokens with the same non-negative integer value can attend to each other; tokens with different values - cannot (cross-chain masking). Padding positions should be set to ``-1``. - When provided, ``attention_mask`` is ignored. The ``flash_attention_2`` backend - only supports single-chain inputs (all non-padding values must be ``0``); pass - multi-chain ``sequence_id`` with ``attn_implementation='sdpa'`` (or ``'eager'``). + cannot (cross-chain masking). Padding positions should be set to ``-1`` and inputs + must be **right-padded** (RoPE uses absolute positions starting at 0). When provided, + ``attention_mask`` is ignored. Passing ``sequence_id`` builds a custom attention + mask, which requires ``torch>=2.6``. Multi-chain inputs additionally require a + non-flash ``attn_implementation`` (``'sdpa'`` / ``'eager'`` / ``'flex_attention'``); + flash attention only supports the single-chain case. output_attentions (`bool`, *optional*): Whether to return the per-block attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -529,84 +369,63 @@ def forward( output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions return_dict = return_dict if return_dict is not None else self.config.return_dict - # Collect per-layer hidden states only when the caller asks for them. - layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) if output_hidden_states else [] - - user_supplied_sequence_id = sequence_id is not None - if not user_supplied_sequence_id and attention_mask is None: - attention_mask = input_ids != self.config.pad_token_id - - x = self.embed(input_ids) - position_ids = torch.arange(x.shape[1], device=x.device).unsqueeze(0) - position_embeddings = self.rotary_emb(x, position_ids) - - if user_supplied_sequence_id: - if self.config._attn_implementation == "flash_attention_2" and (sequence_id > 0).any(): + hidden_states = self.embed(input_ids) + position_ids = torch.arange(hidden_states.shape[1], device=hidden_states.device).unsqueeze(0) + position_embeddings = self.rotary_emb(hidden_states, position_ids) + + if sequence_id is not None: + # Chain-aware attention: a token attends only to tokens sharing its + # ``sequence_id`` (block-diagonal), expressed as an additional ``and`` mask + # over the bidirectional mask. ``attention_mask`` is ignored here -- padding + # is encoded as ``sequence_id == -1``. Flash attention can't represent a + # block-diagonal mask without varlen, so multi-chain inputs are rejected. + if is_flash_attention_requested(self.config) and (sequence_id > 0).any(): raise ValueError( - "Multi-chain ``sequence_id`` (any value > 0) is not " - "supported with attn_implementation='flash_attention_2'. " - "Re-load the model with attn_implementation='sdpa' (or " - "'eager') for chain-aware attention masking." + "Multi-chain ``sequence_id`` (any value > 0) is not supported with " + "flash attention. Re-load the model with attn_implementation='sdpa' " + "(or 'eager' / 'flex_attention') for chain-aware attention masking." ) - # Block-diagonal chain mask: a token attends only to tokens sharing - # its ``sequence_id``. Additive bias broadcast over heads, shape - # ``(batch, 1, seq_len, seq_len)``; handled by the eager / sdpa paths. - same_chain = sequence_id.unsqueeze(-1) == sequence_id.unsqueeze(-2) - attn_bias = torch.zeros(same_chain.shape, dtype=x.dtype, device=x.device).masked_fill_( - ~same_chain, torch.finfo(x.dtype).min + attn_bias = create_bidirectional_mask( + config=self.config, + inputs_embeds=hidden_states, + attention_mask=None, + and_mask_function=packed_sequence_mask_function(sequence_id), ) - attn_bias = attn_bias.unsqueeze(1) else: attn_bias = create_bidirectional_mask( config=self.config, - inputs_embeds=x, + inputs_embeds=hidden_states, attention_mask=attention_mask, ) - last_hidden_state, _, collected, attentions = self.transformer( - x, - attention_mask=attn_bias, - position_embeddings=position_embeddings, - layers_to_collect=layers_to_collect, - output_attentions=output_attentions, - ) + all_hidden_states: tuple[torch.Tensor, ...] = () if output_hidden_states else None + all_attentions: tuple[torch.Tensor, ...] = () if output_attentions else None - # Standard Transformers convention: a tuple of per-layer hidden states (the - # live activations collected in the encoder), not a stacked tensor. Consumers - # that want a single tensor (e.g. ESMFold2's LM projection) stack it themselves. - hidden_states = collected if output_hidden_states else None - - if not return_dict: - return tuple( - v - for v in [ - last_hidden_state, - hidden_states, - attentions, - ] - if v is not None + for layer in self.layers: + if output_hidden_states: + all_hidden_states += (hidden_states,) + hidden_states, attn_weights = layer( + hidden_states, + attn_bias, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, ) + if output_attentions and attn_weights is not None: + all_attentions += (attn_weights,) + + last_hidden_state = self.norm(hidden_states) + if output_hidden_states: + all_hidden_states += (last_hidden_state,) return BaseModelOutput( last_hidden_state=last_hidden_state, - hidden_states=hidden_states, - attentions=attentions, + hidden_states=all_hidden_states, + attentions=all_attentions, ) -# --------------------------------------------------------------------------- -# LM head -# --------------------------------------------------------------------------- - - class ESMCMaskedLMHead(nn.Module): - """Masked-language-modelling head: Linear → GELU → LayerNorm → Linear. - - The published checkpoints store this head as an ``nn.Sequential`` (keys - ``lm_head.{0,2,3}``); the ``esmc`` entry in ``conversion_mapping.py`` remaps - them onto ``dense`` / ``layer_norm`` / ``decoder`` on load (and back on save). - """ - def __init__(self, d_model: int, output_dim: int, hidden_dim: int | None = None) -> None: super().__init__() hidden_dim = hidden_dim if hidden_dim is not None else d_model @@ -621,17 +440,12 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: return self.decoder(hidden_states) -# --------------------------------------------------------------------------- -# Masked language model -# --------------------------------------------------------------------------- - - @auto_docstring class ESMCForMaskedLM(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) self.esmc = ESMCModel(config) - self.lm_head = ESMCMaskedLMHead(config.d_model, config.vocab_size) + self.lm_head = ESMCMaskedLMHead(config.hidden_size, config.vocab_size) self.post_init() def get_output_embeddings(self) -> nn.Linear: @@ -650,7 +464,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, labels: torch.Tensor | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | MaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -701,18 +515,13 @@ def forward( class ESMCClassificationHead(nn.Module): - """Dense classification head applied to the ```` token representation. - - Identical to :class:`~transformers.models.esm.modeling_esm.EsmClassificationHead` - (```` token -> dropout -> ``tanh(dense)`` -> dropout -> ``out_proj``); ESMC just - sources the dropout rate from ``classifier_dropout`` instead of ``hidden_dropout_prob``. - """ + """Head for sentence-level classification tasks.""" def __init__(self, config: ESMCConfig): super().__init__() - self.dense = nn.Linear(config.d_model, config.d_model) + self.dense = nn.Linear(config.hidden_size, config.hidden_size) self.dropout = nn.Dropout(config.classifier_dropout) - self.out_proj = nn.Linear(config.d_model, config.num_labels) + self.out_proj = nn.Linear(config.hidden_size, config.num_labels) def forward(self, features, **kwargs): x = features[:, 0, :] # take token (equiv. to [CLS]) @@ -724,11 +533,6 @@ def forward(self, features, **kwargs): return x -# --------------------------------------------------------------------------- -# Sequence classification -# --------------------------------------------------------------------------- - - @auto_docstring class ESMCForSequenceClassification(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): @@ -747,7 +551,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, labels: torch.Tensor | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: r""" output_attentions (`bool`, *optional*): @@ -798,11 +602,6 @@ def forward( ) -# --------------------------------------------------------------------------- -# Token classification -# --------------------------------------------------------------------------- - - @auto_docstring class ESMCForTokenClassification(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): @@ -810,7 +609,7 @@ def __init__(self, config: ESMCConfig): self.num_labels = config.num_labels self.esmc = ESMCModel(config) self.dropout = nn.Dropout(config.classifier_dropout) - self.classifier = nn.Linear(config.d_model, config.num_labels) + self.classifier = nn.Linear(config.hidden_size, config.num_labels) self.post_init() @can_return_tuple @@ -822,7 +621,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, labels: torch.Tensor | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | TokenClassifierOutput: r""" output_attentions (`bool`, *optional*): diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 83e1b1036704..e0d36f6787a1 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -21,128 +21,61 @@ from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss from torch.nn import functional as F -from ... import initialization as init -from ...masking_utils import create_bidirectional_mask # type: ignore[import] -from ...modeling_outputs import ( # type: ignore[import] +from ...masking_utils import create_bidirectional_mask, packed_sequence_mask_function +from ...modeling_layers import GradientCheckpointingLayer +from ...modeling_outputs import ( BaseModelOutput, MaskedLMOutput, SequenceClassifierOutput, TokenClassifierOutput, ) -from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel # type: ignore[import] -from ...utils import ( # type: ignore[import] +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from ...processing_utils import Unpack +from ...utils import ( + TransformersKwargs, auto_docstring, can_return_tuple, logging, ) -from ..esm.modeling_esm import EsmClassificationHead, EsmRotaryEmbedding, eager_attention_forward - -# RoPE is applied in the activation dtype (no fp32 upcast); Llama's `apply_rotary_pos_emb` -# is that no-upcast variant. (`rotate_half` is pulled in by the modular converter as a dependency.) -from ..llama.modeling_llama import apply_rotary_pos_emb -from ..modernbert.modeling_modernbert import ModernBertMLP +from ...utils.generic import is_flash_attention_requested +from ..esm.modeling_esm import EsmClassificationHead, eager_attention_forward +from ..llama.modeling_llama import LlamaMLP, LlamaRotaryEmbedding, apply_rotary_pos_emb from .configuration_esmc import ESMCConfig logger = logging.get_logger(__name__) -_CONFIG_FOR_DOC = "ESMCConfig" - -# --------------------------------------------------------------------------- -# Rotary position embedding helpers -# --------------------------------------------------------------------------- - - -class ESMCRotaryEmbedding(EsmRotaryEmbedding): - """Rotary position embeddings (RoPE), returning ``(cos, sin)``. - - Two ESMC-specific tweaks over the base implementation: - - * ``inv_freq`` is a **non-persistent** buffer (recomputed from the config, never - stored in the checkpoint). - * ``_apply`` is overridden so ``inv_freq`` stays fp32 even when the module is cast - to bf16/fp16 (e.g. when ESMFold2 loads its bundled ESMC backbone in bf16), so the - rotary frequencies aren't rounded. - """ - - def __init__(self, config: ESMCConfig, device=None): - super().__init__(config, device=device) - inv_freq, _ = self.compute_default_rope_parameters(config, device) - self.register_buffer("inv_freq", inv_freq, persistent=False) +class ESMCRotaryEmbedding(LlamaRotaryEmbedding): def _apply(self, fn, recurse=True): + # Little bit of hackery compared to Llama to match the original's dtypes result = super()._apply(fn, recurse=recurse) inv_freq, _ = self.compute_default_rope_parameters(self.config, device=self.inv_freq.device) self.register_buffer("inv_freq", inv_freq, persistent=False) return result -# --------------------------------------------------------------------------- -# Feed-forward network helpers -# --------------------------------------------------------------------------- - - -def _swiglu_hidden_dim(expansion_ratio: float, d_model: int) -> int: - """Round hidden dim to the nearest multiple of 256 after applying expansion_ratio.""" - return int(((expansion_ratio * d_model) + 255) // 256 * 256) - - -class ESMCMLP(ModernBertMLP): - """SwiGLU feed-forward network reusing :class:`ModernBertMLP`'s gated forward - (``Wo(SiLU(input) * gate)``). Bias-free, no dropout; hidden dim rounded to a - multiple of 256. - """ - - def __init__(self, d_model: int, expansion_ratio: float = 8 / 3) -> None: - nn.Module.__init__(self) - ffn_hidden_size = _swiglu_hidden_dim(expansion_ratio, d_model) - self.Wi = nn.Linear(d_model, 2 * ffn_hidden_size, bias=False) - self.act = nn.SiLU() - self.drop = nn.Identity() - self.Wo = nn.Linear(ffn_hidden_size, d_model, bias=False) - - -# --------------------------------------------------------------------------- -# Attention -# --------------------------------------------------------------------------- +class ESMCMLP(LlamaMLP): + pass class ESMCAttention(nn.Module): - """Multi-head self-attention with QK LayerNorm and RoPE. - - All dims/flags are read from ``config`` (``d_model``/``n_heads``/ - ``qk_layernorm``). ESMC is bias-free, so the linears carry no bias. - """ - def __init__(self, config: ESMCConfig): super().__init__() self.config = config - d_model = config.d_model - n_heads = config.n_heads - self.d_model = d_model - self.n_heads = n_heads - self.d_head = d_model // n_heads - self.scaling = self.d_head**-0.5 + self.head_dim = config.hidden_size // config.num_attention_heads + self.num_attention_heads = config.num_attention_heads + self.scaling = self.head_dim**-0.5 self.attention_dropout = 0.0 - # ESMC is a bidirectional encoder: never apply causal masking. Without - # this, the sdpa/flash interfaces default `is_causal` to True when no - # attention_mask is passed (unpadded inputs), silently masking causally. self.is_causal = False - # Fused QKV projection (``Wqkv``) and output projection (``Wo``), matching - # ModernBERT's attention layout. The pre-attention LayerNorm lives in - # :class:`ESMCLayer` (``attn_norm``); the published checkpoint's fused - # ``attn.layernorm_qkv`` / ``attn.out_proj`` map onto these via - # ``conversion_mapping.py``. - self.Wqkv = nn.Linear(d_model, d_model * 3, bias=False) - self.Wo = nn.Linear(d_model, d_model, bias=False) - - if config.qk_layernorm: - self.q_ln = nn.LayerNorm(d_model, bias=False) - self.k_ln = nn.LayerNorm(d_model, bias=False) - else: - self.q_ln = nn.Identity() - self.k_ln = nn.Identity() + self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) + self.k_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) + self.v_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) + self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False) + + self.q_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() + self.k_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() def forward( self, @@ -150,32 +83,29 @@ def forward( attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, output_attentions: bool = False, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: """Return ``(context, attn_weights)``. Attention is computed by the backend selected through - ``config._attn_implementation`` (``eager`` / ``sdpa`` / - ``flash_attention_2`` / ...). QK-LayerNorm and RoPE (via - ``position_embeddings``) are applied here; the backend only computes - ``softmax(QKᵀ)V``. ``attn_weights`` is ``None`` unless the backend - exposes the probabilities — ``output_attentions=True`` forces the - ``eager`` interface so they are observable. + ``config._attn_implementation``. ``attn_weights`` is ``None`` unless the backend + exposes the probabilities -- ``output_attentions=True`` forces the ``eager`` + interface so they are observable. """ - b, s, _ = hidden_states.shape - qkv = self.Wqkv(hidden_states) - q, k, v = torch.chunk(qkv, 3, dim=-1) - q = self.q_ln(q).to(q.dtype) - k = self.k_ln(k).to(q.dtype) + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_ln(self.q_proj(hidden_states)).to(hidden_states.dtype) + key_states = self.k_ln(self.k_proj(hidden_states)).to(hidden_states.dtype) + value_states = self.v_proj(hidden_states) - # (B, S, D) -> (B, H, S, Dh) - q = q.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - k = k.view(b, s, self.n_heads, self.d_head).transpose(1, 2) - v = v.view(b, s, self.n_heads, self.d_head).transpose(1, 2) + query_states = query_states.view(hidden_shape).transpose(1, 2) + key_states = key_states.view(hidden_shape).transpose(1, 2) + value_states = value_states.view(hidden_shape).transpose(1, 2) if position_embeddings is not None: cos, sin = position_embeddings - q, k = apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1) + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) attention_interface: Callable = eager_attention_forward if not output_attentions: @@ -185,182 +115,74 @@ def forward( attn_output, attn_weights = attention_interface( self, - q, - k, - v, + query_states, + key_states, + value_states, attention_mask, dropout=0.0 if not self.training else self.attention_dropout, scaling=self.scaling, **kwargs, ) - attn_output = attn_output.reshape(b, s, -1) - return self.Wo(attn_output), attn_weights + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + return self.o_proj(attn_output), attn_weights -# --------------------------------------------------------------------------- -# Transformer blocks -# --------------------------------------------------------------------------- - - -class ESMCLayer(nn.Module): - """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling. - - All dims/flags are read from ``config``. The ESM3 residue-scaling factor - (``sqrt(num_hidden_layers / 36)`` when ``config.scale_residue``) is derived - from the config here rather than threaded in. - """ +class ESMCLayer(GradientCheckpointingLayer): + """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling.""" def __init__(self, config: ESMCConfig): super().__init__() - d_model = config.d_model - # Pre-norm layout (cf. ModernBERT): the LayerNorms that the published ESMC - # checkpoint fuses into ``attn.layernorm_qkv`` / ``ffn`` live here as - # ``attn_norm`` / ``mlp_norm`` (remapped in ``conversion_mapping.py``). - self.attn_norm = nn.LayerNorm(d_model) - self.attn = ESMCAttention(config) - self.mlp_norm = nn.LayerNorm(d_model) - self.mlp = ESMCMLP(d_model, config.expansion_ratio) - - self.scaling_factor = math.sqrt(config.n_layers / 36) if config.scale_residue else 1.0 + self.input_layernorm = nn.LayerNorm(config.hidden_size) + self.self_attn = ESMCAttention(config) + self.post_attention_layernorm = nn.LayerNorm(config.hidden_size) + self.mlp = ESMCMLP(config) + self.scaling_factor = math.sqrt(config.num_hidden_layers / 36) if config.scale_residue else 1.0 def forward( self, - x: torch.Tensor, + hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, output_attentions: bool = False, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: - """ - Args: - x: ``(batch, seq_len, d_model)`` - attention_mask: Additive attention bias broadcastable to - ``(batch, num_heads, seq_len, seq_len)``, or ``None`` for full - (unmasked) attention. - output_attentions: When ``True``, returns the per-head attention - weights for this block alongside the residual output. - - Returns: - ``(output, attn_weights_or_None)``. Shape of ``output`` is - ``(batch, seq_len, d_model)``; ``attn_weights`` shape is - ``(batch, num_heads, seq_len, seq_len)`` or ``None``. - """ - attn_out, attn_weights = self.attn( - self.attn_norm(x), + residual = hidden_states + attn_output, attn_weights = self.self_attn( + self.input_layernorm(hidden_states), attention_mask, position_embeddings=position_embeddings, output_attentions=output_attentions, **kwargs, ) - x = x + attn_out / self.scaling_factor - x = x + self.mlp(self.mlp_norm(x)) / self.scaling_factor - return x, attn_weights - - -class ESMCEncoder(nn.Module): - """Stack of :class:`ESMCLayer` layers with a final LayerNorm. All dims/flags - are read from ``config``.""" - - def __init__(self, config: ESMCConfig): - super().__init__() - self.blocks = nn.ModuleList([ESMCLayer(config) for _ in range(config.n_layers)]) - self.norm = nn.LayerNorm(config.d_model, bias=False) - - def forward( - self, - x: torch.Tensor, - attention_mask: torch.Tensor | None = None, - position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, - layers_to_collect: list[int] | None = None, - output_attentions: bool = False, - **kwargs, - ) -> tuple[ - torch.Tensor, - torch.Tensor, - tuple[torch.Tensor, ...], - tuple[torch.Tensor, ...] | None, - ]: - """Run the full transformer stack. - - Args: - x: ``(batch, seq_len, d_model)`` - attention_mask: Additive attention bias forwarded to each block, or - ``None`` for full attention. - layers_to_collect: Layer indices (0-based pre-block inputs plus - ``n_layers`` for the post-norm output) whose hidden states - should be returned. - output_attentions: When ``True``, collects the per-block attention - weights and returns them as the fourth tuple element. - - Returns: - ``(post_norm, pre_norm, hidden_states, attentions)`` where - ``hidden_states`` is a (possibly empty) tuple of tensors and - ``attentions`` is a tuple of per-block ``(B, H, L, L)`` tensors - or ``None`` when ``output_attentions`` is ``False``. - """ - if layers_to_collect is None: - layers_to_collect = [] - - collected: list[torch.Tensor] = [] - all_attentions: list[torch.Tensor] = [] - for layer_idx, block in enumerate(self.blocks): - if layer_idx in layers_to_collect: - collected.append(x) - x, attn_weights = block( - x, - attention_mask, - position_embeddings=position_embeddings, - output_attentions=output_attentions, - **kwargs, - ) - if output_attentions and attn_weights is not None: - all_attentions.append(attn_weights) - - norm_x = self.norm(x) - if len(self.blocks) in layers_to_collect: - collected.append(norm_x) - - attentions = tuple(all_attentions) if output_attentions else None - return norm_x, x, tuple(collected), attentions - - -# --------------------------------------------------------------------------- -# Pre-trained model base class -# --------------------------------------------------------------------------- + # ESM3 residue scaling on each residual branch. + hidden_states = residual + attn_output / self.scaling_factor + residual = hidden_states + hidden_states = residual + self.mlp(self.post_attention_layernorm(hidden_states)) / self.scaling_factor + return hidden_states, attn_weights @auto_docstring class ESMCPreTrainedModel(PreTrainedModel): config_class = ESMCConfig base_model_prefix = "esmc" - supports_gradient_checkpointing = False + supports_gradient_checkpointing = True _supports_sdpa = True _supports_flash_attn = True _supports_attention_backend = True _no_split_modules = ["ESMCLayer"] - _keys_to_ignore_on_load_unexpected = [r"\._extra_state$"] - - def _init_weights(self, module: nn.Module): - if isinstance(module, ESMCRotaryEmbedding): - inv_freq, _ = module.compute_default_rope_parameters(module.config) - init.copy_(module.inv_freq, inv_freq) - else: - # nn.Linear / nn.Embedding / nn.LayerNorm via the base initializer. - super()._init_weights(module) - - -# --------------------------------------------------------------------------- -# Base encoder model -# --------------------------------------------------------------------------- + # ``inv_freq`` / ``original_inv_freq`` are non-persistent rotary buffers; ``_extra_state`` + # keys come from the published checkpoint's fused TransformerEngine layout. + _keys_to_ignore_on_load_unexpected = [r"\._extra_state$", r"\.inv_freq$", r"\.original_inv_freq$"] @auto_docstring class ESMCModel(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) - self.embed = nn.Embedding(config.vocab_size, config.d_model) + self.embed = nn.Embedding(config.vocab_size, config.hidden_size) self.rotary_emb = ESMCRotaryEmbedding(config) - self.transformer = ESMCEncoder(config) + self.layers = nn.ModuleList([ESMCLayer(config) for _ in range(config.num_hidden_layers)]) + self.norm = nn.LayerNorm(config.hidden_size, bias=False) self.post_init() def get_input_embeddings(self) -> nn.Embedding: @@ -379,16 +201,18 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, return_dict: bool | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor for chain-aware attention masking. Tokens with the same non-negative integer value can attend to each other; tokens with different values - cannot (cross-chain masking). Padding positions should be set to ``-1``. - When provided, ``attention_mask`` is ignored. The ``flash_attention_2`` backend - only supports single-chain inputs (all non-padding values must be ``0``); pass - multi-chain ``sequence_id`` with ``attn_implementation='sdpa'`` (or ``'eager'``). + cannot (cross-chain masking). Padding positions should be set to ``-1`` and inputs + must be **right-padded** (RoPE uses absolute positions starting at 0). When provided, + ``attention_mask`` is ignored. Passing ``sequence_id`` builds a custom attention + mask, which requires ``torch>=2.6``. Multi-chain inputs additionally require a + non-flash ``attn_implementation`` (``'sdpa'`` / ``'eager'`` / ``'flex_attention'``); + flash attention only supports the single-chain case. output_attentions (`bool`, *optional*): Whether to return the per-block attention weights of shape ``(batch_size, num_heads, sequence_length, sequence_length)``. @@ -415,84 +239,63 @@ def forward( output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions return_dict = return_dict if return_dict is not None else self.config.return_dict - # Collect per-layer hidden states only when the caller asks for them. - layers_to_collect: list[int] = list(range(self.config.n_layers + 1)) if output_hidden_states else [] - - user_supplied_sequence_id = sequence_id is not None - if not user_supplied_sequence_id and attention_mask is None: - attention_mask = input_ids != self.config.pad_token_id - - x = self.embed(input_ids) - position_ids = torch.arange(x.shape[1], device=x.device).unsqueeze(0) - position_embeddings = self.rotary_emb(x, position_ids) - - if user_supplied_sequence_id: - if self.config._attn_implementation == "flash_attention_2" and (sequence_id > 0).any(): + hidden_states = self.embed(input_ids) + position_ids = torch.arange(hidden_states.shape[1], device=hidden_states.device).unsqueeze(0) + position_embeddings = self.rotary_emb(hidden_states, position_ids) + + if sequence_id is not None: + # Chain-aware attention: a token attends only to tokens sharing its + # ``sequence_id`` (block-diagonal), expressed as an additional ``and`` mask + # over the bidirectional mask. ``attention_mask`` is ignored here -- padding + # is encoded as ``sequence_id == -1``. Flash attention can't represent a + # block-diagonal mask without varlen, so multi-chain inputs are rejected. + if is_flash_attention_requested(self.config) and (sequence_id > 0).any(): raise ValueError( - "Multi-chain ``sequence_id`` (any value > 0) is not " - "supported with attn_implementation='flash_attention_2'. " - "Re-load the model with attn_implementation='sdpa' (or " - "'eager') for chain-aware attention masking." + "Multi-chain ``sequence_id`` (any value > 0) is not supported with " + "flash attention. Re-load the model with attn_implementation='sdpa' " + "(or 'eager' / 'flex_attention') for chain-aware attention masking." ) - # Block-diagonal chain mask: a token attends only to tokens sharing - # its ``sequence_id``. Additive bias broadcast over heads, shape - # ``(batch, 1, seq_len, seq_len)``; handled by the eager / sdpa paths. - same_chain = sequence_id.unsqueeze(-1) == sequence_id.unsqueeze(-2) - attn_bias = torch.zeros(same_chain.shape, dtype=x.dtype, device=x.device).masked_fill_( - ~same_chain, torch.finfo(x.dtype).min + attn_bias = create_bidirectional_mask( + config=self.config, + inputs_embeds=hidden_states, + attention_mask=None, + and_mask_function=packed_sequence_mask_function(sequence_id), ) - attn_bias = attn_bias.unsqueeze(1) else: attn_bias = create_bidirectional_mask( config=self.config, - inputs_embeds=x, + inputs_embeds=hidden_states, attention_mask=attention_mask, ) - last_hidden_state, _, collected, attentions = self.transformer( - x, - attention_mask=attn_bias, - position_embeddings=position_embeddings, - layers_to_collect=layers_to_collect, - output_attentions=output_attentions, - ) + all_hidden_states: tuple[torch.Tensor, ...] = () if output_hidden_states else None + all_attentions: tuple[torch.Tensor, ...] = () if output_attentions else None - # Standard Transformers convention: a tuple of per-layer hidden states (the - # live activations collected in the encoder), not a stacked tensor. Consumers - # that want a single tensor (e.g. ESMFold2's LM projection) stack it themselves. - hidden_states = collected if output_hidden_states else None - - if not return_dict: - return tuple( - v - for v in [ - last_hidden_state, - hidden_states, - attentions, - ] - if v is not None + for layer in self.layers: + if output_hidden_states: + all_hidden_states += (hidden_states,) + hidden_states, attn_weights = layer( + hidden_states, + attn_bias, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, ) + if output_attentions and attn_weights is not None: + all_attentions += (attn_weights,) + + last_hidden_state = self.norm(hidden_states) + if output_hidden_states: + all_hidden_states += (last_hidden_state,) return BaseModelOutput( last_hidden_state=last_hidden_state, - hidden_states=hidden_states, - attentions=attentions, + hidden_states=all_hidden_states, + attentions=all_attentions, ) -# --------------------------------------------------------------------------- -# LM head -# --------------------------------------------------------------------------- - - class ESMCMaskedLMHead(nn.Module): - """Masked-language-modelling head: Linear → GELU → LayerNorm → Linear. - - The published checkpoints store this head as an ``nn.Sequential`` (keys - ``lm_head.{0,2,3}``); the ``esmc`` entry in ``conversion_mapping.py`` remaps - them onto ``dense`` / ``layer_norm`` / ``decoder`` on load (and back on save). - """ - def __init__(self, d_model: int, output_dim: int, hidden_dim: int | None = None) -> None: super().__init__() hidden_dim = hidden_dim if hidden_dim is not None else d_model @@ -507,17 +310,12 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: return self.decoder(hidden_states) -# --------------------------------------------------------------------------- -# Masked language model -# --------------------------------------------------------------------------- - - @auto_docstring class ESMCForMaskedLM(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) self.esmc = ESMCModel(config) - self.lm_head = ESMCMaskedLMHead(config.d_model, config.vocab_size) + self.lm_head = ESMCMaskedLMHead(config.hidden_size, config.vocab_size) self.post_init() def get_output_embeddings(self) -> nn.Linear: @@ -536,7 +334,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, labels: torch.Tensor | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | MaskedLMOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): @@ -586,29 +384,12 @@ def forward( ) -# --------------------------------------------------------------------------- -# Classification heads -# --------------------------------------------------------------------------- - - class ESMCClassificationHead(EsmClassificationHead): - """Dense classification head applied to the ```` token representation. - - Identical to :class:`~transformers.models.esm.modeling_esm.EsmClassificationHead` - (```` token -> dropout -> ``tanh(dense)`` -> dropout -> ``out_proj``); ESMC just - sources the dropout rate from ``classifier_dropout`` instead of ``hidden_dropout_prob``. - """ - def __init__(self, config: ESMCConfig): super().__init__() - self.dense = nn.Linear(config.d_model, config.d_model) + self.dense = nn.Linear(config.hidden_size, config.hidden_size) self.dropout = nn.Dropout(config.classifier_dropout) - self.out_proj = nn.Linear(config.d_model, config.num_labels) - - -# --------------------------------------------------------------------------- -# Sequence classification -# --------------------------------------------------------------------------- + self.out_proj = nn.Linear(config.hidden_size, config.num_labels) @auto_docstring @@ -629,7 +410,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, labels: torch.Tensor | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: r""" output_attentions (`bool`, *optional*): @@ -680,11 +461,6 @@ def forward( ) -# --------------------------------------------------------------------------- -# Token classification -# --------------------------------------------------------------------------- - - @auto_docstring class ESMCForTokenClassification(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): @@ -692,7 +468,7 @@ def __init__(self, config: ESMCConfig): self.num_labels = config.num_labels self.esmc = ESMCModel(config) self.dropout = nn.Dropout(config.classifier_dropout) - self.classifier = nn.Linear(config.d_model, config.num_labels) + self.classifier = nn.Linear(config.hidden_size, config.num_labels) self.post_init() @can_return_tuple @@ -704,7 +480,7 @@ def forward( output_hidden_states: bool | None = None, output_attentions: bool | None = None, labels: torch.Tensor | None = None, - **kwargs, + **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | TokenClassifierOutput: r""" output_attentions (`bool`, *optional*): diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index c28ca1a3c425..2f4916c3916b 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -17,8 +17,8 @@ from tokenizers.models import BPE from tokenizers.processors import TemplateProcessing -from ...tokenization_utils_fast import PreTrainedTokenizerFast # type: ignore[import] -from ...utils import logging # type: ignore[import] +from ...tokenization_utils_fast import PreTrainedTokenizerFast +from ...utils import logging logger = logging.get_logger(__name__) @@ -83,6 +83,7 @@ class ESMCTokenizer(PreTrainedTokenizerFast): The mask token, used for masked language modelling. eos_token (`str`, *optional*, defaults to `""`): The end-of-sequence token (appended to every sequence). + bos_token (``, *optional*): chain_break_token (`str`, *optional*, defaults to `"|"`): Token inserted between chains in multi-chain protein inputs. @@ -107,6 +108,7 @@ def __init__( pad_token="", mask_token="", eos_token="", + bos_token=None, chain_break_token="|", **kwargs, ): @@ -119,6 +121,8 @@ def __init__( pad_token = self._ensure_str(pad_token) mask_token = self._ensure_str(mask_token) eos_token = self._ensure_str(eos_token) + # ESMC uses as the sequence-start token, so `bos_token` defaults to it. + bos_token = self._ensure_str(bos_token) if bos_token is not None else cls_token chain_break_token = self._ensure_str(chain_break_token) bpe = BPE(token_to_id, merges=[], unk_token=unk_token) @@ -154,6 +158,7 @@ def __init__( super().__init__( tokenizer_object=tokenizer, unk_token=unk_token, + bos_token=bos_token, cls_token=cls_token, pad_token=pad_token, mask_token=mask_token, @@ -161,20 +166,6 @@ def __init__( **kwargs, ) - # ------------------------------------------------------------------ - # bos / cls aliases - # The model uses as the sequence-start token; the HF base class - # expects `bos_token`. We alias them to avoid confusion. - # ------------------------------------------------------------------ - - @property - def bos_token(self): - return self.cls_token - - @property - def bos_token_id(self): - return self.cls_token_id - # ------------------------------------------------------------------ # Chain-break token # ------------------------------------------------------------------ @@ -190,18 +181,6 @@ def chain_break_token_id(self) -> int: raise TypeError(f"Expected a single token id for the chain-break token, got {token_id!r}.") return token_id - # ------------------------------------------------------------------ - # Convenience helpers used by downstream code - # ------------------------------------------------------------------ - - @property - def all_token_ids(self): - return list(range(self.vocab_size)) - - @property - def special_token_ids(self): - return self.all_special_ids - # ------------------------------------------------------------------ # Internal utilities # ------------------------------------------------------------------ From 1ccebba55e2898b4c7217ffc09b4a12a416ca9b9 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jun 2026 16:14:20 +0100 Subject: [PATCH 58/70] Fix the tokenizer to match Llama --- .../models/auto/tokenization_auto.py | 4 +-- .../models/esmc/tokenization_esmc.py | 28 ++++++------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/transformers/models/auto/tokenization_auto.py b/src/transformers/models/auto/tokenization_auto.py index 05a1f2f4e286..bcad6bdb3980 100644 --- a/src/transformers/models/auto/tokenization_auto.py +++ b/src/transformers/models/auto/tokenization_auto.py @@ -119,8 +119,8 @@ ("emu3", "GPT2Tokenizer" if is_tokenizers_available() else None), ("ernie", "BertTokenizer" if is_tokenizers_available() else None), ("esm", "EsmTokenizer"), - ("esmc", "ESMCTokenizer"), - ("esmfold2", "ESMCTokenizer"), + ("esmc", "ESMCTokenizer" if is_tokenizers_available() else None), + ("esmfold2", "ESMCTokenizer" if is_tokenizers_available() else None), ("falcon_mamba", "GPTNeoXTokenizer" if is_tokenizers_available() else None), ("fastspeech2_conformer", "FastSpeech2ConformerTokenizer" if is_g2p_en_available() else None), ("flaubert", "FlaubertTokenizer"), diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index 2f4916c3916b..da2cafa8ef9f 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -17,7 +17,7 @@ from tokenizers.models import BPE from tokenizers.processors import TemplateProcessing -from ...tokenization_utils_fast import PreTrainedTokenizerFast +from ...tokenization_utils_tokenizers import TokenizersBackend from ...utils import logging @@ -64,7 +64,7 @@ ] -class ESMCTokenizer(PreTrainedTokenizerFast): +class ESMCTokenizer(TokenizersBackend): r""" Construct an ESMC tokenizer. @@ -100,6 +100,7 @@ class ESMCTokenizer(PreTrainedTokenizerFast): vocab_files_names = VOCAB_FILES_NAMES model_input_names = ["input_ids", "attention_mask"] + model = BPE def __init__( self, @@ -112,8 +113,7 @@ def __init__( chain_break_token="|", **kwargs, ): - all_tokens = SEQUENCE_VOCAB - token_to_id = {tok: ind for ind, tok in enumerate(all_tokens)} + token_to_id = {tok: ind for ind, tok in enumerate(SEQUENCE_VOCAB)} # Normalise: always work with plain strings unk_token = self._ensure_str(unk_token) @@ -125,24 +125,15 @@ def __init__( bos_token = self._ensure_str(bos_token) if bos_token is not None else cls_token chain_break_token = self._ensure_str(chain_break_token) - bpe = BPE(token_to_id, merges=[], unk_token=unk_token) - tokenizer = Tokenizer(bpe) - - special_tokens = [ - cls_token, - pad_token, - mask_token, - eos_token, - chain_break_token, - ] - tokenizer.add_special_tokens(special_tokens) + self._tokenizer = Tokenizer(BPE(token_to_id, merges=[], unk_token=unk_token)) + self._tokenizer.add_special_tokens([cls_token, pad_token, mask_token, eos_token, chain_break_token]) # Automatically wrap every encoded sequence with . - tokenizer.post_processor = TemplateProcessing( + self._tokenizer.post_processor = TemplateProcessing( single=f"{cls_token} $A {eos_token}", special_tokens=[ - (cls_token, tokenizer.token_to_id(cls_token)), - (eos_token, tokenizer.token_to_id(eos_token)), + (cls_token, self._tokenizer.token_to_id(cls_token)), + (eos_token, self._tokenizer.token_to_id(eos_token)), ], ) @@ -156,7 +147,6 @@ def __init__( self._chain_break_token = chain_break_token super().__init__( - tokenizer_object=tokenizer, unk_token=unk_token, bos_token=bos_token, cls_token=cls_token, From e9afa1934bb527ed0764991d47c03e20029207b0 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jun 2026 18:40:00 +0100 Subject: [PATCH 59/70] Make the token classifier generic, attention cleanup, big modular reductions for ESMC --- src/transformers/conversion_mapping.py | 1 + .../models/esmc/configuration_esmc.py | 12 ++ src/transformers/models/esmc/modeling_esmc.py | 183 +++++------------- src/transformers/models/esmc/modular_esmc.py | 179 ++++------------- 4 files changed, 98 insertions(+), 277 deletions(-) diff --git a/src/transformers/conversion_mapping.py b/src/transformers/conversion_mapping.py index 8d666d5fa20f..efac601b4e3b 100755 --- a/src/transformers/conversion_mapping.py +++ b/src/transformers/conversion_mapping.py @@ -791,6 +791,7 @@ def _build_checkpoint_conversion_mapping(): ), ], "esmc": [ + WeightRenaming(r"embed\.", "embed_tokens."), WeightRenaming(r"transformer\.blocks", "layers"), # The negative lookbehinds anchor the *reverse* search to the final encoder # norm only (they are stripped from the forward replacement), so saving does diff --git a/src/transformers/models/esmc/configuration_esmc.py b/src/transformers/models/esmc/configuration_esmc.py index 24aec33d635c..421ec5cfa4d4 100644 --- a/src/transformers/models/esmc/configuration_esmc.py +++ b/src/transformers/models/esmc/configuration_esmc.py @@ -28,6 +28,9 @@ class ESMCConfig(PreTrainedConfig): Dimensionality of the encoder layers and the pooler layer. num_attention_heads (`int`, *optional*, defaults to 40): Number of attention heads for each attention layer in the Transformer encoder. + num_key_value_heads (`int`, *optional*): + Number of key/value heads. ESMC uses standard multi-head attention (no GQA), so this + defaults to `num_attention_heads`. num_hidden_layers (`int`, *optional*, defaults to 80): Number of hidden layers in the Transformer encoder. mask_token_id (`int`, *optional*, defaults to 32): @@ -50,6 +53,10 @@ class ESMCConfig(PreTrainedConfig): derived from `expansion_ratio` (see above). hidden_act (`str`, *optional*, defaults to `"silu"`): The non-linear activation function in the feed-forward network. + attention_bias (`bool`, *optional*, defaults to `False`): + Whether the attention query/key/value/output projections use a bias (ESMC is bias-free). + attention_dropout (`float`, *optional*, defaults to 0.0): + Dropout ratio on the attention probabilities. qk_layernorm (`bool`, *optional*, defaults to `True`): Whether to apply LayerNorm to queries and keys before computing attention. scale_residue (`bool`, *optional*, defaults to `True`): @@ -83,6 +90,7 @@ class ESMCConfig(PreTrainedConfig): vocab_size: int | None = 64 hidden_size: int | None = 2560 num_attention_heads: int | None = 40 + num_key_value_heads: int | None = None num_hidden_layers: int | None = 80 pad_token_id: int | None = 1 mask_token_id: int | None = 32 @@ -94,12 +102,16 @@ class ESMCConfig(PreTrainedConfig): intermediate_size: int | None = None hidden_act: str | None = "silu" mlp_bias: bool | None = False + attention_bias: bool | None = False + attention_dropout: float | None = 0.0 qk_layernorm: bool | None = True scale_residue: bool | None = True tie_word_embeddings: bool | None = False def __post_init__(self, **kwargs): super().__post_init__(**kwargs) + if self.num_key_value_heads is None: + self.num_key_value_heads = self.num_attention_heads if self.intermediate_size is None: self.intermediate_size = int(((self.expansion_ratio * self.hidden_size) + 255) // 256 * 256) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index da5991a722bd..41280621a1c1 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -28,15 +28,16 @@ from torch.nn import functional as F from ...activations import ACT2FN -from ...integrations import use_kernel_func_from_hub +from ...integrations import use_kernel_func_from_hub, use_kernelized_func from ...masking_utils import create_bidirectional_mask, packed_sequence_mask_function -from ...modeling_layers import GradientCheckpointingLayer -from ...modeling_outputs import BaseModelOutput, MaskedLMOutput, SequenceClassifierOutput, TokenClassifierOutput +from ...modeling_layers import GenericForTokenClassification, GradientCheckpointingLayer +from ...modeling_outputs import BaseModelOutput, MaskedLMOutput, SequenceClassifierOutput from ...modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack from ...utils import TransformersKwargs, auto_docstring, can_return_tuple from ...utils.generic import is_flash_attention_requested, maybe_autocast +from ...utils.output_capturing import capture_outputs from .configuration_esmc import ESMCConfig @@ -189,21 +190,32 @@ def eager_attention_forward( return attn_output, attn_weights +@use_kernelized_func(apply_rotary_pos_emb) class ESMCAttention(nn.Module): - def __init__(self, config: ESMCConfig): + """Multi-head self-attention with QK-LayerNorm and RoPE.""" + + def __init__(self, config: ESMCConfig, layer_idx: int | None = None): super().__init__() self.config = config - self.head_dim = config.hidden_size // config.num_attention_heads - self.num_attention_heads = config.num_attention_heads + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads self.scaling = self.head_dim**-0.5 - self.attention_dropout = 0.0 + self.attention_dropout = config.attention_dropout self.is_causal = False - self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) - self.k_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) - self.v_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) - self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False) - + self.q_proj = nn.Linear( + config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias + ) self.q_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() self.k_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() @@ -212,16 +224,8 @@ def forward( hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, - output_attentions: bool = False, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: - """Return ``(context, attn_weights)``. - - Attention is computed by the backend selected through - ``config._attn_implementation``. ``attn_weights`` is ``None`` unless the backend - exposes the probabilities -- ``output_attentions=True`` forces the ``eager`` - interface so they are observable. - """ input_shape = hidden_states.shape[:-1] hidden_shape = (*input_shape, -1, self.head_dim) @@ -237,11 +241,9 @@ def forward( cos, sin = position_embeddings query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) - attention_interface: Callable = eager_attention_forward - if not output_attentions: - attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( - self.config._attn_implementation, eager_attention_forward - ) + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) attn_output, attn_weights = attention_interface( self, @@ -273,22 +275,20 @@ def forward( hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, - output_attentions: bool = False, **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, torch.Tensor | None]: + ) -> torch.Tensor: residual = hidden_states - attn_output, attn_weights = self.self_attn( + attn_output, _ = self.self_attn( self.input_layernorm(hidden_states), attention_mask, position_embeddings=position_embeddings, - output_attentions=output_attentions, **kwargs, ) # ESM3 residue scaling on each residual branch. hidden_states = residual + attn_output / self.scaling_factor residual = hidden_states hidden_states = residual + self.mlp(self.post_attention_layernorm(hidden_states)) / self.scaling_factor - return hidden_states, attn_weights + return hidden_states @auto_docstring @@ -300,6 +300,10 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_flash_attn = True _supports_attention_backend = True _no_split_modules = ["ESMCLayer"] + _can_record_outputs = { + "hidden_states": ESMCLayer, + "attentions": ESMCAttention, + } # ``inv_freq`` / ``original_inv_freq`` are non-persistent rotary buffers; ``_extra_state`` # keys come from the published checkpoint's fused TransformerEngine layout. _keys_to_ignore_on_load_unexpected = [r"\._extra_state$", r"\.inv_freq$", r"\.original_inv_freq$"] @@ -309,30 +313,21 @@ class ESMCPreTrainedModel(PreTrainedModel): class ESMCModel(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) - self.embed = nn.Embedding(config.vocab_size, config.hidden_size) + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size) self.rotary_emb = ESMCRotaryEmbedding(config) self.layers = nn.ModuleList([ESMCLayer(config) for _ in range(config.num_hidden_layers)]) self.norm = nn.LayerNorm(config.hidden_size, bias=False) self.post_init() - def get_input_embeddings(self) -> nn.Embedding: - return self.embed - - def set_input_embeddings(self, value: nn.Embedding): - self.embed = value - - @can_return_tuple + @capture_outputs @auto_docstring def forward( self, input_ids: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, sequence_id: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, - return_dict: bool | None = None, **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, ...] | BaseModelOutput: + ) -> BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor for chain-aware attention masking. Tokens with the same @@ -343,12 +338,6 @@ def forward( mask, which requires ``torch>=2.6``. Multi-chain inputs additionally require a non-flash ``attn_implementation`` (``'sdpa'`` / ``'eager'`` / ``'flex_attention'``); flash attention only supports the single-chain case. - output_attentions (`bool`, *optional*): - Whether to return the per-block attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Forces a manual-SDPA path inside :class:`ESMCAttention` so the - attention probabilities are observable; raises on the - ``flash_attention_2`` path. Examples: @@ -363,13 +352,7 @@ def forward( torch.Size([1, 12, 960]) ``` """ - output_hidden_states = ( - output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states - ) - output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions - return_dict = return_dict if return_dict is not None else self.config.return_dict - - hidden_states = self.embed(input_ids) + hidden_states = self.embed_tokens(input_ids) position_ids = torch.arange(hidden_states.shape[1], device=hidden_states.device).unsqueeze(0) position_embeddings = self.rotary_emb(hidden_states, position_ids) @@ -398,31 +381,17 @@ def forward( attention_mask=attention_mask, ) - all_hidden_states: tuple[torch.Tensor, ...] = () if output_hidden_states else None - all_attentions: tuple[torch.Tensor, ...] = () if output_attentions else None - for layer in self.layers: - if output_hidden_states: - all_hidden_states += (hidden_states,) - hidden_states, attn_weights = layer( + hidden_states = layer( hidden_states, attn_bias, position_embeddings=position_embeddings, - output_attentions=output_attentions, **kwargs, ) - if output_attentions and attn_weights is not None: - all_attentions += (attn_weights,) - last_hidden_state = self.norm(hidden_states) - if output_hidden_states: - all_hidden_states += (last_hidden_state,) + output = self.norm(hidden_states) - return BaseModelOutput( - last_hidden_state=last_hidden_state, - hidden_states=all_hidden_states, - attentions=all_attentions, - ) + return BaseModelOutput(last_hidden_state=output) class ESMCMaskedLMHead(nn.Module): @@ -461,8 +430,6 @@ def forward( input_ids: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, sequence_id: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, labels: torch.Tensor | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | MaskedLMOutput: @@ -470,9 +437,6 @@ def forward( sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor forwarded to the encoder for chain-aware attention masking. See :meth:`ESMCModel.forward` for the encoding. - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): Labels for masked language modelling loss. Positions with label ``-100`` are ignored. Other positions must be in ``[0, config.vocab_size)``. @@ -495,9 +459,8 @@ def forward( input_ids=input_ids, attention_mask=attention_mask, sequence_id=sequence_id, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, return_dict=True, + **kwargs, ) logits = self.lm_head(encoder_outputs.last_hidden_state) @@ -548,15 +511,10 @@ def forward( self, input_ids: torch.LongTensor | None = None, attention_mask: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, labels: torch.Tensor | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: r""" - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): Labels for sequence classification loss. Indices must be in ``[0, config.num_labels - 1]``. For regression pass a float @@ -565,9 +523,8 @@ def forward( encoder_outputs = self.esmc( input_ids, attention_mask=attention_mask, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, return_dict=True, + **kwargs, ) logits = self.classifier(encoder_outputs.last_hidden_state) @@ -602,58 +559,8 @@ def forward( ) -@auto_docstring -class ESMCForTokenClassification(ESMCPreTrainedModel): - def __init__(self, config: ESMCConfig): - super().__init__(config) - self.num_labels = config.num_labels - self.esmc = ESMCModel(config) - self.dropout = nn.Dropout(config.classifier_dropout) - self.classifier = nn.Linear(config.hidden_size, config.num_labels) - self.post_init() - - @can_return_tuple - @auto_docstring - def forward( - self, - input_ids: torch.Tensor | None = None, - attention_mask: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, - labels: torch.Tensor | None = None, - **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, ...] | TokenClassifierOutput: - r""" - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. - Positions with index ``-100`` are ignored in the loss. - """ - encoder_outputs = self.esmc( - input_ids=input_ids, - attention_mask=attention_mask, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, - return_dict=True, - ) - - sequence_output = self.dropout(encoder_outputs.last_hidden_state) - logits = self.classifier(sequence_output) - - loss: torch.Tensor | None = None - if labels is not None: - loss = CrossEntropyLoss(ignore_index=-100)( - logits.view(-1, self.num_labels), labels.to(logits.device).view(-1) - ) - - return TokenClassifierOutput( - loss=loss, - logits=logits, - hidden_states=encoder_outputs.hidden_states, - attentions=encoder_outputs.attentions, - ) +class ESMCForTokenClassification(GenericForTokenClassification, ESMCPreTrainedModel): + pass __all__ = [ diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index e0d36f6787a1..615965109d0c 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -22,12 +22,11 @@ from torch.nn import functional as F from ...masking_utils import create_bidirectional_mask, packed_sequence_mask_function -from ...modeling_layers import GradientCheckpointingLayer +from ...modeling_layers import GenericForTokenClassification, GradientCheckpointingLayer from ...modeling_outputs import ( BaseModelOutput, MaskedLMOutput, SequenceClassifierOutput, - TokenClassifierOutput, ) from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack @@ -38,8 +37,9 @@ logging, ) from ...utils.generic import is_flash_attention_requested +from ...utils.output_capturing import capture_outputs from ..esm.modeling_esm import EsmClassificationHead, eager_attention_forward -from ..llama.modeling_llama import LlamaMLP, LlamaRotaryEmbedding, apply_rotary_pos_emb +from ..llama.modeling_llama import LlamaAttention, LlamaMLP, LlamaRotaryEmbedding, apply_rotary_pos_emb from .configuration_esmc import ESMCConfig @@ -59,21 +59,19 @@ class ESMCMLP(LlamaMLP): pass -class ESMCAttention(nn.Module): - def __init__(self, config: ESMCConfig): - super().__init__() - self.config = config - self.head_dim = config.hidden_size // config.num_attention_heads - self.num_attention_heads = config.num_attention_heads - self.scaling = self.head_dim**-0.5 - self.attention_dropout = 0.0 - self.is_causal = False +class ESMCAttention(LlamaAttention): + """Multi-head self-attention with QK-LayerNorm and RoPE. - self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) - self.k_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) - self.v_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False) - self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False) + Inherits Llama's projection setup -- ``q/k/v/o_proj`` are bias-free + (``config.attention_bias=False``) and there is no GQA + (``num_key_value_heads == num_attention_heads``). On top of Llama it applies + QK-LayerNorm to the projected query/key (before the head reshape) and runs + bidirectionally (``is_causal=False``). + """ + def __init__(self, config: ESMCConfig, layer_idx: int | None = None): + super().__init__(config, layer_idx) + self.is_causal = False self.q_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() self.k_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() @@ -82,15 +80,13 @@ def forward( hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, - output_attentions: bool = False, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: """Return ``(context, attn_weights)``. - Attention is computed by the backend selected through - ``config._attn_implementation``. ``attn_weights`` is ``None`` unless the backend - exposes the probabilities -- ``output_attentions=True`` forces the ``eager`` - interface so they are observable. + Attention is computed by the backend selected through ``config._attn_implementation``. + ``attn_weights`` is captured by the output recorder when requested and is only populated + by backends that expose the probabilities (e.g. ``eager``). """ input_shape = hidden_states.shape[:-1] hidden_shape = (*input_shape, -1, self.head_dim) @@ -107,11 +103,9 @@ def forward( cos, sin = position_embeddings query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) - attention_interface: Callable = eager_attention_forward - if not output_attentions: - attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface( - self.config._attn_implementation, eager_attention_forward - ) + attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( + self.config._attn_implementation, eager_attention_forward + ) attn_output, attn_weights = attention_interface( self, @@ -143,22 +137,20 @@ def forward( hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, - output_attentions: bool = False, **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, torch.Tensor | None]: + ) -> torch.Tensor: residual = hidden_states - attn_output, attn_weights = self.self_attn( + attn_output, _ = self.self_attn( self.input_layernorm(hidden_states), attention_mask, position_embeddings=position_embeddings, - output_attentions=output_attentions, **kwargs, ) # ESM3 residue scaling on each residual branch. hidden_states = residual + attn_output / self.scaling_factor residual = hidden_states hidden_states = residual + self.mlp(self.post_attention_layernorm(hidden_states)) / self.scaling_factor - return hidden_states, attn_weights + return hidden_states @auto_docstring @@ -170,6 +162,10 @@ class ESMCPreTrainedModel(PreTrainedModel): _supports_flash_attn = True _supports_attention_backend = True _no_split_modules = ["ESMCLayer"] + _can_record_outputs = { + "hidden_states": ESMCLayer, + "attentions": ESMCAttention, + } # ``inv_freq`` / ``original_inv_freq`` are non-persistent rotary buffers; ``_extra_state`` # keys come from the published checkpoint's fused TransformerEngine layout. _keys_to_ignore_on_load_unexpected = [r"\._extra_state$", r"\.inv_freq$", r"\.original_inv_freq$"] @@ -179,30 +175,21 @@ class ESMCPreTrainedModel(PreTrainedModel): class ESMCModel(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) - self.embed = nn.Embedding(config.vocab_size, config.hidden_size) + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size) self.rotary_emb = ESMCRotaryEmbedding(config) self.layers = nn.ModuleList([ESMCLayer(config) for _ in range(config.num_hidden_layers)]) self.norm = nn.LayerNorm(config.hidden_size, bias=False) self.post_init() - def get_input_embeddings(self) -> nn.Embedding: - return self.embed - - def set_input_embeddings(self, value: nn.Embedding): - self.embed = value - - @can_return_tuple + @capture_outputs @auto_docstring def forward( self, input_ids: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, sequence_id: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, - return_dict: bool | None = None, **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, ...] | BaseModelOutput: + ) -> BaseModelOutput: r""" sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor for chain-aware attention masking. Tokens with the same @@ -213,12 +200,6 @@ def forward( mask, which requires ``torch>=2.6``. Multi-chain inputs additionally require a non-flash ``attn_implementation`` (``'sdpa'`` / ``'eager'`` / ``'flex_attention'``); flash attention only supports the single-chain case. - output_attentions (`bool`, *optional*): - Whether to return the per-block attention weights of shape - ``(batch_size, num_heads, sequence_length, sequence_length)``. - Forces a manual-SDPA path inside :class:`ESMCAttention` so the - attention probabilities are observable; raises on the - ``flash_attention_2`` path. Examples: @@ -233,13 +214,7 @@ def forward( torch.Size([1, 12, 960]) ``` """ - output_hidden_states = ( - output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states - ) - output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions - return_dict = return_dict if return_dict is not None else self.config.return_dict - - hidden_states = self.embed(input_ids) + hidden_states = self.embed_tokens(input_ids) position_ids = torch.arange(hidden_states.shape[1], device=hidden_states.device).unsqueeze(0) position_embeddings = self.rotary_emb(hidden_states, position_ids) @@ -268,31 +243,17 @@ def forward( attention_mask=attention_mask, ) - all_hidden_states: tuple[torch.Tensor, ...] = () if output_hidden_states else None - all_attentions: tuple[torch.Tensor, ...] = () if output_attentions else None - for layer in self.layers: - if output_hidden_states: - all_hidden_states += (hidden_states,) - hidden_states, attn_weights = layer( + hidden_states = layer( hidden_states, attn_bias, position_embeddings=position_embeddings, - output_attentions=output_attentions, **kwargs, ) - if output_attentions and attn_weights is not None: - all_attentions += (attn_weights,) - last_hidden_state = self.norm(hidden_states) - if output_hidden_states: - all_hidden_states += (last_hidden_state,) + output = self.norm(hidden_states) - return BaseModelOutput( - last_hidden_state=last_hidden_state, - hidden_states=all_hidden_states, - attentions=all_attentions, - ) + return BaseModelOutput(last_hidden_state=output) class ESMCMaskedLMHead(nn.Module): @@ -331,8 +292,6 @@ def forward( input_ids: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, sequence_id: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, labels: torch.Tensor | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | MaskedLMOutput: @@ -340,9 +299,6 @@ def forward( sequence_id (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): Integer chain-ID tensor forwarded to the encoder for chain-aware attention masking. See :meth:`ESMCModel.forward` for the encoding. - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): Labels for masked language modelling loss. Positions with label ``-100`` are ignored. Other positions must be in ``[0, config.vocab_size)``. @@ -365,9 +321,8 @@ def forward( input_ids=input_ids, attention_mask=attention_mask, sequence_id=sequence_id, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, return_dict=True, + **kwargs, ) logits = self.lm_head(encoder_outputs.last_hidden_state) @@ -407,15 +362,10 @@ def forward( self, input_ids: torch.LongTensor | None = None, attention_mask: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, labels: torch.Tensor | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: r""" - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): Labels for sequence classification loss. Indices must be in ``[0, config.num_labels - 1]``. For regression pass a float @@ -424,9 +374,8 @@ def forward( encoder_outputs = self.esmc( input_ids, attention_mask=attention_mask, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, return_dict=True, + **kwargs, ) logits = self.classifier(encoder_outputs.last_hidden_state) @@ -461,58 +410,10 @@ def forward( ) -@auto_docstring -class ESMCForTokenClassification(ESMCPreTrainedModel): - def __init__(self, config: ESMCConfig): - super().__init__(config) - self.num_labels = config.num_labels - self.esmc = ESMCModel(config) - self.dropout = nn.Dropout(config.classifier_dropout) - self.classifier = nn.Linear(config.hidden_size, config.num_labels) - self.post_init() - - @can_return_tuple - @auto_docstring - def forward( - self, - input_ids: torch.Tensor | None = None, - attention_mask: torch.Tensor | None = None, - output_hidden_states: bool | None = None, - output_attentions: bool | None = None, - labels: torch.Tensor | None = None, - **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, ...] | TokenClassifierOutput: - r""" - output_attentions (`bool`, *optional*): - Whether to return per-block attention weights. Forwarded to the - backbone; raises on the ``flash_attention_2`` path. - labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): - Per-token labels. Indices must be in ``[0, config.num_labels - 1]``. - Positions with index ``-100`` are ignored in the loss. - """ - encoder_outputs = self.esmc( - input_ids=input_ids, - attention_mask=attention_mask, - output_hidden_states=output_hidden_states, - output_attentions=output_attentions, - return_dict=True, - ) - - sequence_output = self.dropout(encoder_outputs.last_hidden_state) - logits = self.classifier(sequence_output) - - loss: torch.Tensor | None = None - if labels is not None: - loss = CrossEntropyLoss(ignore_index=-100)( - logits.view(-1, self.num_labels), labels.to(logits.device).view(-1) - ) - - return TokenClassifierOutput( - loss=loss, - logits=logits, - hidden_states=encoder_outputs.hidden_states, - attentions=encoder_outputs.attentions, - ) +class ESMCForTokenClassification(GenericForTokenClassification, ESMCPreTrainedModel): + # ``dropout`` (from ``config.classifier_dropout``) + a ``score`` linear over the + # per-token hidden states, identical to the ESM token-classification head. + pass __all__ = [ From c884d5e6f4f9dddb8091b0239780c083ac043363 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jun 2026 18:49:05 +0100 Subject: [PATCH 60/70] Import the sequence classifier --- src/transformers/models/esmc/modeling_esmc.py | 67 +++++++++++++------ src/transformers/models/esmc/modular_esmc.py | 65 ++---------------- 2 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 41280621a1c1..4532770cf854 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -192,7 +192,14 @@ def eager_attention_forward( @use_kernelized_func(apply_rotary_pos_emb) class ESMCAttention(nn.Module): - """Multi-head self-attention with QK-LayerNorm and RoPE.""" + """Multi-head self-attention with QK-LayerNorm and RoPE. + + Inherits Llama's projection setup -- ``q/k/v/o_proj`` are bias-free + (``config.attention_bias=False``) and there is no GQA + (``num_key_value_heads == num_attention_heads``). On top of Llama it applies + QK-LayerNorm to the projected query/key (before the head reshape) and runs + bidirectionally (``is_causal=False``). + """ def __init__(self, config: ESMCConfig, layer_idx: int | None = None): super().__init__() @@ -226,6 +233,12 @@ def forward( position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: + """Return ``(context, attn_weights)``. + + Attention is computed by the backend selected through ``config._attn_implementation``. + ``attn_weights`` is captured by the output recorder when requested and is only populated + by backends that expose the probabilities (e.g. ``eager``). + """ input_shape = hidden_states.shape[:-1] hidden_shape = (*input_shape, -1, self.head_dim) @@ -496,7 +509,12 @@ def forward(self, features, **kwargs): return x -@auto_docstring +@auto_docstring( + custom_intro=""" + ESMC Model transformer with a sequence classification/regression head on top (a linear layer on top of the pooled + output) e.g. for GLUE tasks. + """ +) class ESMCForSequenceClassification(ESMCPreTrainedModel): def __init__(self, config: ESMCConfig): super().__init__(config) @@ -511,55 +529,64 @@ def forward( self, input_ids: torch.LongTensor | None = None, attention_mask: torch.Tensor | None = None, - labels: torch.Tensor | None = None, + position_ids: torch.LongTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + labels: torch.LongTensor | None = None, **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: + ) -> tuple | SequenceClassifierOutput: r""" labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): - Labels for sequence classification loss. Indices must be in - ``[0, config.num_labels - 1]``. For regression pass a float - tensor of shape ``(batch_size,)``. + Labels for computing the sequence classification/regression loss. Indices should be in `[0, ..., + config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If + `config.num_labels > 1` a classification loss is computed (Cross-Entropy). """ - encoder_outputs = self.esmc( + + outputs = self.esmc( input_ids, attention_mask=attention_mask, - return_dict=True, + position_ids=position_ids, + inputs_embeds=inputs_embeds, **kwargs, ) - logits = self.classifier(encoder_outputs.last_hidden_state) + sequence_output = outputs[0] + logits = self.classifier(sequence_output) - loss: torch.Tensor | None = None + loss = None if labels is not None: labels = labels.to(logits.device) if self.config.problem_type is None: if self.num_labels == 1: self.config.problem_type = "regression" - elif self.num_labels > 1 and labels.dtype in (torch.long, torch.int): + elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int): self.config.problem_type = "single_label_classification" else: self.config.problem_type = "multi_label_classification" if self.config.problem_type == "regression": loss_fct = MSELoss() - loss = loss_fct( - logits.squeeze() if self.num_labels == 1 else logits, - labels.squeeze() if self.num_labels == 1 else labels, - ) + if self.num_labels == 1: + loss = loss_fct(logits.squeeze(), labels.squeeze()) + else: + loss = loss_fct(logits, labels) elif self.config.problem_type == "single_label_classification": - loss = CrossEntropyLoss()(logits.view(-1, self.num_labels), labels.view(-1)) + loss_fct = CrossEntropyLoss() + loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) elif self.config.problem_type == "multi_label_classification": - loss = BCEWithLogitsLoss()(logits, labels) + loss_fct = BCEWithLogitsLoss() + loss = loss_fct(logits, labels) return SequenceClassifierOutput( loss=loss, logits=logits, - hidden_states=encoder_outputs.hidden_states, - attentions=encoder_outputs.attentions, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, ) class ESMCForTokenClassification(GenericForTokenClassification, ESMCPreTrainedModel): + # ``dropout`` (from ``config.classifier_dropout``) + a ``score`` linear over the + # per-token hidden states, identical to the ESM token-classification head. pass diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 615965109d0c..95df8168cf8e 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -18,7 +18,7 @@ import torch import torch.nn as nn -from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss +from torch.nn import CrossEntropyLoss from torch.nn import functional as F from ...masking_utils import create_bidirectional_mask, packed_sequence_mask_function @@ -26,7 +26,6 @@ from ...modeling_outputs import ( BaseModelOutput, MaskedLMOutput, - SequenceClassifierOutput, ) from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack @@ -38,7 +37,7 @@ ) from ...utils.generic import is_flash_attention_requested from ...utils.output_capturing import capture_outputs -from ..esm.modeling_esm import EsmClassificationHead, eager_attention_forward +from ..esm.modeling_esm import EsmClassificationHead, EsmForSequenceClassification, eager_attention_forward from ..llama.modeling_llama import LlamaAttention, LlamaMLP, LlamaRotaryEmbedding, apply_rotary_pos_emb from .configuration_esmc import ESMCConfig @@ -347,68 +346,16 @@ def __init__(self, config: ESMCConfig): self.out_proj = nn.Linear(config.hidden_size, config.num_labels) -@auto_docstring -class ESMCForSequenceClassification(ESMCPreTrainedModel): +class ESMCForSequenceClassification(EsmForSequenceClassification): + # Same ``-token classification head + problem-type loss as ESM-2; only the + # backbone (no `add_pooling_layer`) and the `classifier_dropout`-sourced head differ. def __init__(self, config: ESMCConfig): - super().__init__(config) + ESMCPreTrainedModel.__init__(self, config) self.num_labels = config.num_labels self.esmc = ESMCModel(config) self.classifier = ESMCClassificationHead(config) self.post_init() - @can_return_tuple - @auto_docstring - def forward( - self, - input_ids: torch.LongTensor | None = None, - attention_mask: torch.Tensor | None = None, - labels: torch.Tensor | None = None, - **kwargs: Unpack[TransformersKwargs], - ) -> tuple[torch.Tensor, ...] | SequenceClassifierOutput: - r""" - labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): - Labels for sequence classification loss. Indices must be in - ``[0, config.num_labels - 1]``. For regression pass a float - tensor of shape ``(batch_size,)``. - """ - encoder_outputs = self.esmc( - input_ids, - attention_mask=attention_mask, - return_dict=True, - **kwargs, - ) - logits = self.classifier(encoder_outputs.last_hidden_state) - - loss: torch.Tensor | None = None - if labels is not None: - labels = labels.to(logits.device) - - if self.config.problem_type is None: - if self.num_labels == 1: - self.config.problem_type = "regression" - elif self.num_labels > 1 and labels.dtype in (torch.long, torch.int): - self.config.problem_type = "single_label_classification" - else: - self.config.problem_type = "multi_label_classification" - - if self.config.problem_type == "regression": - loss_fct = MSELoss() - loss = loss_fct( - logits.squeeze() if self.num_labels == 1 else logits, - labels.squeeze() if self.num_labels == 1 else labels, - ) - elif self.config.problem_type == "single_label_classification": - loss = CrossEntropyLoss()(logits.view(-1, self.num_labels), labels.view(-1)) - elif self.config.problem_type == "multi_label_classification": - loss = BCEWithLogitsLoss()(logits, labels) - - return SequenceClassifierOutput( - loss=loss, - logits=logits, - hidden_states=encoder_outputs.hidden_states, - attentions=encoder_outputs.attentions, - ) - class ESMCForTokenClassification(GenericForTokenClassification, ESMCPreTrainedModel): # ``dropout`` (from ``config.classifier_dropout``) + a ``score`` linear over the From 284ad3dd574ca52b199d9f9f5af34fc4ed17e53e Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jun 2026 17:35:20 +0100 Subject: [PATCH 61/70] More review fixes --- docs/source/en/model_doc/esmc.md | 26 ++- src/transformers/conversion_mapping.py | 4 +- .../models/esmc/configuration_esmc.py | 105 ++++++----- src/transformers/models/esmc/modeling_esmc.py | 82 ++++---- src/transformers/models/esmc/modular_esmc.py | 177 ++++++++++++------ tests/models/esmc/test_modeling_esmc.py | 62 +++++- tests/models/esmc/test_tokenization_esmc.py | 50 ++--- utils/check_config_attributes.py | 1 + 8 files changed, 328 insertions(+), 179 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index d46117992024..532d9527a359 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -34,13 +34,32 @@ Pre-trained checkpoints are available on the Hugging Face Hub: ## Usage example +ESMC is registered with the auto classes (`AutoModel`, `AutoModelForMaskedLM`, +`AutoModelForSequenceClassification`, `AutoModelForTokenClassification`). + + + + +```python +import torch +from transformers import pipeline + +extractor = pipeline( + task="feature-extraction", + model="biohub/ESMC-300M", +) +# Per-residue representations of shape (batch, sequence_length, hidden_size). +representations = extractor("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ", return_tensors="pt") +``` + + + + ```python import torch from transformers import AutoModel, AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") -# ESMC is registered with the auto classes (AutoModel, AutoModelForMaskedLM, -# AutoModelForSequenceClassification, AutoModelForTokenClassification). model = AutoModel.from_pretrained("biohub/ESMC-300M") inputs = tokenizer("MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ", return_tensors="pt") @@ -51,6 +70,9 @@ with torch.no_grad(): representations = outputs.last_hidden_state ``` + + + ## ESMCConfig [[autodoc]] ESMCConfig diff --git a/src/transformers/conversion_mapping.py b/src/transformers/conversion_mapping.py index efac601b4e3b..81a7e927b879 100755 --- a/src/transformers/conversion_mapping.py +++ b/src/transformers/conversion_mapping.py @@ -799,8 +799,8 @@ def _build_checkpoint_conversion_mapping(): WeightRenaming(r"transformer\.norm\.", r"(?"``), used for masked language modelling. classifier_dropout (`float`, *optional*, defaults to 0.1): Dropout ratio for the classification head. - max_position_embeddings (`int`, *optional*, defaults to 2048): - Nominal maximum sequence length. RoPE imposes no hard limit, so this only sizes - the rotary cache for dynamic RoPE variants (unused by the default RoPE). - rope_parameters (`RopeParameters` or `dict`, *optional*): - Dictionary configuring the rotary position embeddings (RoPE). When omitted, the - default RoPE is used with `rope_theta` = `default_theta` (10000.0). See - [`~modeling_rope_utils.RopeParameters`] for the accepted keys. expansion_ratio (`float`, *optional*, defaults to `8/3`): Hidden-dim expansion ratio for the SwiGLU feed-forward network. When `intermediate_size` is not given it is derived from this as `expansion_ratio * hidden_size` rounded up to a multiple of 256. - intermediate_size (`int`, *optional*): - Dimensionality of the SwiGLU feed-forward layer. Defaults to the value - derived from `expansion_ratio` (see above). - hidden_act (`str`, *optional*, defaults to `"silu"`): - The non-linear activation function in the feed-forward network. - attention_bias (`bool`, *optional*, defaults to `False`): - Whether the attention query/key/value/output projections use a bias (ESMC is bias-free). - attention_dropout (`float`, *optional*, defaults to 0.0): - Dropout ratio on the attention probabilities. qk_layernorm (`bool`, *optional*, defaults to `True`): Whether to apply LayerNorm to queries and keys before computing attention. scale_residue (`bool`, *optional*, defaults to `True`): @@ -80,40 +59,78 @@ class ESMCConfig(PreTrainedConfig): """ model_type = "esmc" - default_theta = 10000.0 + keys_to_ignore_at_inference = ["past_key_values"] + # Default tensor parallel plan for base model `ESMCModel` + base_model_tp_plan = { + "layers.*.self_attn.q_proj": "colwise", + "layers.*.self_attn.k_proj": "colwise", + "layers.*.self_attn.v_proj": "colwise", + "layers.*.self_attn.o_proj": "rowwise", + "layers.*.mlp.gate_proj": "colwise", + "layers.*.mlp.up_proj": "colwise", + "layers.*.mlp.down_proj": "rowwise", + } + base_model_pp_plan = { + "embed_tokens": (["input_ids"], ["inputs_embeds"]), + "layers": (["hidden_states", "attention_mask"], ["hidden_states"]), + "norm": (["hidden_states"], ["hidden_states"]), + } + + # Llama fields re-declared with ESMC defaults. + vocab_size: int = 64 + hidden_size: int = 2560 + intermediate_size: int | None = None + num_hidden_layers: int = 80 + num_attention_heads: int = 40 + num_key_value_heads: int | None = None + hidden_act: str = "silu" + max_position_embeddings: int = 2048 + initializer_range: float = 0.02 + use_cache: bool = True + pad_token_id: int | None = 1 + bos_token_id: int | None = None + eos_token_id: int | list[int] | None = None + tie_word_embeddings: bool = False + rope_parameters: RopeParameters | dict | None = None + attention_bias: bool = False + attention_dropout: float | int = 0.0 + mlp_bias: bool = False + head_dim: int | None = None attribute_map = { "d_model": "hidden_size", "n_heads": "num_attention_heads", "n_layers": "num_hidden_layers", } - vocab_size: int | None = 64 - hidden_size: int | None = 2560 - num_attention_heads: int | None = 40 - num_key_value_heads: int | None = None - num_hidden_layers: int | None = 80 - pad_token_id: int | None = 1 + # ESMC-specific fields. mask_token_id: int | None = 32 - initializer_range: float | None = 0.02 classifier_dropout: float | None = 0.1 - max_position_embeddings: int | None = 2048 - rope_parameters: RopeParameters | dict | None = None expansion_ratio: float | None = 8 / 3 - intermediate_size: int | None = None - hidden_act: str | None = "silu" - mlp_bias: bool | None = False - attention_bias: bool | None = False - attention_dropout: float | None = 0.0 qk_layernorm: bool | None = True scale_residue: bool | None = True - tie_word_embeddings: bool | None = False def __post_init__(self, **kwargs): - super().__post_init__(**kwargs) + if self.head_dim is None: + self.head_dim = self.hidden_size // self.num_attention_heads if self.num_key_value_heads is None: self.num_key_value_heads = self.num_attention_heads + + super().__post_init__(**kwargs) + # ``attribute_map`` (``d_model``/``n_heads``) is only applied inside the base ``__post_init__``, + # so the GQA-free key/value head count and head dim must be (re-)derived afterwards -- ESMC never + # uses grouped-query attention, so ``num_key_value_heads`` always tracks ``num_attention_heads``. + self.num_key_value_heads = self.num_attention_heads + self.head_dim = self.hidden_size // self.num_attention_heads if self.intermediate_size is None: self.intermediate_size = int(((self.expansion_ratio * self.hidden_size) + 255) // 256 * 256) + def validate_architecture(self): + """Part of `@strict`-powered validation. Validates the architecture of the config.""" + if self.hidden_size % self.num_attention_heads != 0: + raise ValueError( + f"The hidden size ({self.hidden_size}) is not a multiple of the number of attention " + f"heads ({self.num_attention_heads})." + ) + __all__ = ["ESMCConfig"] diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index 4532770cf854..ab2720ebeccb 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -36,7 +36,7 @@ from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack from ...utils import TransformersKwargs, auto_docstring, can_return_tuple -from ...utils.generic import is_flash_attention_requested, maybe_autocast +from ...utils.generic import is_flash_attention_requested, maybe_autocast, merge_with_config_defaults from ...utils.output_capturing import capture_outputs from .configuration_esmc import ESMCConfig @@ -105,13 +105,6 @@ def forward(self, x, position_ids): return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) - def _apply(self, fn, recurse=True): - # Little bit of hackery compared to Llama to match the original's dtypes - result = super()._apply(fn, recurse=recurse) - inv_freq, _ = self.compute_default_rope_parameters(self.config, device=self.inv_freq.device) - self.register_buffer("inv_freq", inv_freq, persistent=False) - return result - class ESMCMLP(nn.Module): def __init__(self, config): @@ -192,14 +185,7 @@ def eager_attention_forward( @use_kernelized_func(apply_rotary_pos_emb) class ESMCAttention(nn.Module): - """Multi-head self-attention with QK-LayerNorm and RoPE. - - Inherits Llama's projection setup -- ``q/k/v/o_proj`` are bias-free - (``config.attention_bias=False``) and there is no GQA - (``num_key_value_heads == num_attention_heads``). On top of Llama it applies - QK-LayerNorm to the projected query/key (before the head reshape) and runs - bidirectionally (``is_causal=False``). - """ + """Multi-head self-attention with QK-LayerNorm and RoPE.""" def __init__(self, config: ESMCConfig, layer_idx: int | None = None): super().__init__() @@ -223,8 +209,8 @@ def __init__(self, config: ESMCConfig, layer_idx: int | None = None): self.o_proj = nn.Linear( config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias ) - self.q_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() - self.k_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() + self.q_norm = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() + self.k_norm = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() def forward( self, @@ -233,26 +219,19 @@ def forward( position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: - """Return ``(context, attn_weights)``. - - Attention is computed by the backend selected through ``config._attn_implementation``. - ``attn_weights`` is captured by the output recorder when requested and is only populated - by backends that expose the probabilities (e.g. ``eager``). - """ input_shape = hidden_states.shape[:-1] hidden_shape = (*input_shape, -1, self.head_dim) - query_states = self.q_ln(self.q_proj(hidden_states)).to(hidden_states.dtype) - key_states = self.k_ln(self.k_proj(hidden_states)).to(hidden_states.dtype) + query_states = self.q_norm(self.q_proj(hidden_states)).to(hidden_states.dtype) + key_states = self.k_norm(self.k_proj(hidden_states)).to(hidden_states.dtype) value_states = self.v_proj(hidden_states) query_states = query_states.view(hidden_shape).transpose(1, 2) key_states = key_states.view(hidden_shape).transpose(1, 2) value_states = value_states.view(hidden_shape).transpose(1, 2) - if position_embeddings is not None: - cos, sin = position_embeddings - query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( self.config._attn_implementation, eager_attention_forward @@ -275,12 +254,17 @@ def forward( class ESMCLayer(GradientCheckpointingLayer): """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling.""" - def __init__(self, config: ESMCConfig): + def __init__(self, config: ESMCConfig, layer_idx: int | None = None): super().__init__() + self.hidden_size = config.hidden_size + + self.self_attn = ESMCAttention(config=config, layer_idx=layer_idx) + + self.mlp = ESMCMLP(config) + # LayerNorm instead of Llama's RMSNorm. self.input_layernorm = nn.LayerNorm(config.hidden_size) - self.self_attn = ESMCAttention(config) self.post_attention_layernorm = nn.LayerNorm(config.hidden_size) - self.mlp = ESMCMLP(config) + # ESM3 residual scaling to stabilise deep networks. self.scaling_factor = math.sqrt(config.num_hidden_layers / 36) if config.scale_residue else 1.0 def forward( @@ -309,10 +293,10 @@ class ESMCPreTrainedModel(PreTrainedModel): config_class = ESMCConfig base_model_prefix = "esmc" supports_gradient_checkpointing = True - _supports_sdpa = True _supports_flash_attn = True + _supports_sdpa = True + _supports_flex_attn = True _supports_attention_backend = True - _no_split_modules = ["ESMCLayer"] _can_record_outputs = { "hidden_states": ESMCLayer, "attentions": ESMCAttention, @@ -320,6 +304,7 @@ class ESMCPreTrainedModel(PreTrainedModel): # ``inv_freq`` / ``original_inv_freq`` are non-persistent rotary buffers; ``_extra_state`` # keys come from the published checkpoint's fused TransformerEngine layout. _keys_to_ignore_on_load_unexpected = [r"\._extra_state$", r"\.inv_freq$", r"\.original_inv_freq$"] + _no_split_modules = ["ESMCLayer"] @auto_docstring @@ -330,8 +315,10 @@ def __init__(self, config: ESMCConfig): self.rotary_emb = ESMCRotaryEmbedding(config) self.layers = nn.ModuleList([ESMCLayer(config) for _ in range(config.num_hidden_layers)]) self.norm = nn.LayerNorm(config.hidden_size, bias=False) + self.gradient_checkpointing = False self.post_init() + @merge_with_config_defaults @capture_outputs @auto_docstring def forward( @@ -370,25 +357,28 @@ def forward( position_embeddings = self.rotary_emb(hidden_states, position_ids) if sequence_id is not None: - # Chain-aware attention: a token attends only to tokens sharing its - # ``sequence_id`` (block-diagonal), expressed as an additional ``and`` mask - # over the bidirectional mask. ``attention_mask`` is ignored here -- padding - # is encoded as ``sequence_id == -1``. Flash attention can't represent a - # block-diagonal mask without varlen, so multi-chain inputs are rejected. - if is_flash_attention_requested(self.config) and (sequence_id > 0).any(): + # Chain-aware attention: a token attends only to tokens sharing its ``sequence_id`` + # (block-diagonal), expressed as an additional ``and`` mask over the bidirectional mask. + # ``attention_mask`` is ignored here -- padding is encoded as ``sequence_id == -1``. + # Flash attention can't represent this block-diagonal mask: it would be silently dropped, + # letting tokens attend across chains (and to padding). So ``sequence_id`` requires a + # non-flash backend. This is a config-only check (no data inspection), so it stays + # ``torch.compile`` / ``torch.export`` friendly -- unlike inspecting the data to decide, + # which would either break tracing or silently miss the drop under a captured graph. + if is_flash_attention_requested(self.config): raise ValueError( - "Multi-chain ``sequence_id`` (any value > 0) is not supported with " - "flash attention. Re-load the model with attn_implementation='sdpa' " - "(or 'eager' / 'flex_attention') for chain-aware attention masking." + "`sequence_id` (chain-aware attention) is not supported with flash attention, " + "which can't represent the block-diagonal chain mask. Re-load the model with " + "attn_implementation='sdpa' (or 'eager' / 'flex_attention')." ) - attn_bias = create_bidirectional_mask( + attention_mask = create_bidirectional_mask( config=self.config, inputs_embeds=hidden_states, attention_mask=None, and_mask_function=packed_sequence_mask_function(sequence_id), ) else: - attn_bias = create_bidirectional_mask( + attention_mask = create_bidirectional_mask( config=self.config, inputs_embeds=hidden_states, attention_mask=attention_mask, @@ -397,7 +387,7 @@ def forward( for layer in self.layers: hidden_states = layer( hidden_states, - attn_bias, + attention_mask, position_embeddings=position_embeddings, **kwargs, ) diff --git a/src/transformers/models/esmc/modular_esmc.py b/src/transformers/models/esmc/modular_esmc.py index 95df8168cf8e..8282a9027efc 100644 --- a/src/transformers/models/esmc/modular_esmc.py +++ b/src/transformers/models/esmc/modular_esmc.py @@ -18,16 +18,18 @@ import torch import torch.nn as nn +from huggingface_hub.dataclasses import strict from torch.nn import CrossEntropyLoss from torch.nn import functional as F from ...masking_utils import create_bidirectional_mask, packed_sequence_mask_function -from ...modeling_layers import GenericForTokenClassification, GradientCheckpointingLayer +from ...modeling_layers import GenericForTokenClassification from ...modeling_outputs import ( BaseModelOutput, MaskedLMOutput, ) -from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from ...modeling_rope_utils import RopeParameters +from ...modeling_utils import ALL_ATTENTION_FUNCTIONS from ...processing_utils import Unpack from ...utils import ( TransformersKwargs, @@ -35,23 +37,107 @@ can_return_tuple, logging, ) -from ...utils.generic import is_flash_attention_requested +from ...utils.generic import is_flash_attention_requested, merge_with_config_defaults from ...utils.output_capturing import capture_outputs from ..esm.modeling_esm import EsmClassificationHead, EsmForSequenceClassification, eager_attention_forward -from ..llama.modeling_llama import LlamaAttention, LlamaMLP, LlamaRotaryEmbedding, apply_rotary_pos_emb -from .configuration_esmc import ESMCConfig +from ..llama.configuration_llama import LlamaConfig +from ..llama.modeling_llama import ( + LlamaAttention, + LlamaDecoderLayer, + LlamaMLP, + LlamaRotaryEmbedding, + apply_rotary_pos_emb, +) +from ..nomic_bert.modeling_nomic_bert import NomicBertPreTrainedModel logger = logging.get_logger(__name__) +@auto_docstring(checkpoint="biohub/ESMC-6B") +@strict +class ESMCConfig(LlamaConfig): + r""" + mask_token_id (`int`, *optional*, defaults to 32): + Index of the mask token in the vocabulary (``""``), used for masked language modelling. + classifier_dropout (`float`, *optional*, defaults to 0.1): + Dropout ratio for the classification head. + expansion_ratio (`float`, *optional*, defaults to `8/3`): + Hidden-dim expansion ratio for the SwiGLU feed-forward network. When + `intermediate_size` is not given it is derived from this as + `expansion_ratio * hidden_size` rounded up to a multiple of 256. + qk_layernorm (`bool`, *optional*, defaults to `True`): + Whether to apply LayerNorm to queries and keys before computing attention. + scale_residue (`bool`, *optional*, defaults to `True`): + Whether to apply ESM3 residual scaling (`1 / sqrt(num_hidden_layers / 36)` + per block) to stabilise deep networks. + + Examples: + + ```python + >>> from transformers import ESMCConfig, ESMCModel + + >>> # Initializing an ESMC biohub/ESMC-6B style configuration + >>> configuration = ESMCConfig() + + >>> # Initializing a model (with random weights) from the configuration + >>> model = ESMCModel(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ``` + """ + + model_type = "esmc" + attribute_map = { + "d_model": "hidden_size", + "n_heads": "num_attention_heads", + "n_layers": "num_hidden_layers", + } + + # Llama fields re-declared with ESMC defaults. + vocab_size: int = 64 + hidden_size: int = 2560 + intermediate_size: int | None = None + num_hidden_layers: int = 80 + num_attention_heads: int = 40 + num_key_value_heads: int | None = None + hidden_act: str = "silu" + max_position_embeddings: int = 2048 + initializer_range: float = 0.02 + pad_token_id: int | None = 1 + bos_token_id: int | None = None + eos_token_id: int | list[int] | None = None + tie_word_embeddings: bool = False + rope_parameters: RopeParameters | dict | None = None + attention_bias: bool = False + attention_dropout: float | int = 0.0 + mlp_bias: bool = False + + # ESMC-specific fields. + mask_token_id: int | None = 32 + classifier_dropout: float | None = 0.1 + expansion_ratio: float | None = 8 / 3 + qk_layernorm: bool | None = True + scale_residue: bool | None = True + + # Llama fields that do not apply to ESMC (LayerNorm, not RMSNorm; no tensor-parallel pretraining). + rms_norm_eps = AttributeError() + pretraining_tp = AttributeError() + + def __post_init__(self, **kwargs): + super().__post_init__(**kwargs) + # ``attribute_map`` (``d_model``/``n_heads``) is only applied inside the base ``__post_init__``, + # so the GQA-free key/value head count and head dim must be (re-)derived afterwards -- ESMC never + # uses grouped-query attention, so ``num_key_value_heads`` always tracks ``num_attention_heads``. + self.num_key_value_heads = self.num_attention_heads + self.head_dim = self.hidden_size // self.num_attention_heads + if self.intermediate_size is None: + self.intermediate_size = int(((self.expansion_ratio * self.hidden_size) + 255) // 256 * 256) + + class ESMCRotaryEmbedding(LlamaRotaryEmbedding): - def _apply(self, fn, recurse=True): - # Little bit of hackery compared to Llama to match the original's dtypes - result = super()._apply(fn, recurse=recurse) - inv_freq, _ = self.compute_default_rope_parameters(self.config, device=self.inv_freq.device) - self.register_buffer("inv_freq", inv_freq, persistent=False) - return result + pass class ESMCMLP(LlamaMLP): @@ -59,20 +145,13 @@ class ESMCMLP(LlamaMLP): class ESMCAttention(LlamaAttention): - """Multi-head self-attention with QK-LayerNorm and RoPE. - - Inherits Llama's projection setup -- ``q/k/v/o_proj`` are bias-free - (``config.attention_bias=False``) and there is no GQA - (``num_key_value_heads == num_attention_heads``). On top of Llama it applies - QK-LayerNorm to the projected query/key (before the head reshape) and runs - bidirectionally (``is_causal=False``). - """ + """Multi-head self-attention with QK-LayerNorm and RoPE.""" def __init__(self, config: ESMCConfig, layer_idx: int | None = None): super().__init__(config, layer_idx) self.is_causal = False - self.q_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() - self.k_ln = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() + self.q_norm = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() + self.k_norm = nn.LayerNorm(config.hidden_size, bias=False) if config.qk_layernorm else nn.Identity() def forward( self, @@ -81,26 +160,19 @@ def forward( position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, **kwargs: Unpack[TransformersKwargs], ) -> tuple[torch.Tensor, torch.Tensor | None]: - """Return ``(context, attn_weights)``. - - Attention is computed by the backend selected through ``config._attn_implementation``. - ``attn_weights`` is captured by the output recorder when requested and is only populated - by backends that expose the probabilities (e.g. ``eager``). - """ input_shape = hidden_states.shape[:-1] hidden_shape = (*input_shape, -1, self.head_dim) - query_states = self.q_ln(self.q_proj(hidden_states)).to(hidden_states.dtype) - key_states = self.k_ln(self.k_proj(hidden_states)).to(hidden_states.dtype) + query_states = self.q_norm(self.q_proj(hidden_states)).to(hidden_states.dtype) + key_states = self.k_norm(self.k_proj(hidden_states)).to(hidden_states.dtype) value_states = self.v_proj(hidden_states) query_states = query_states.view(hidden_shape).transpose(1, 2) key_states = key_states.view(hidden_shape).transpose(1, 2) value_states = value_states.view(hidden_shape).transpose(1, 2) - if position_embeddings is not None: - cos, sin = position_embeddings - query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface( self.config._attn_implementation, eager_attention_forward @@ -120,15 +192,15 @@ def forward( return self.o_proj(attn_output), attn_weights -class ESMCLayer(GradientCheckpointingLayer): +class ESMCLayer(LlamaDecoderLayer): """Single transformer block: pre-norm attention + pre-norm FFN with residual scaling.""" - def __init__(self, config: ESMCConfig): - super().__init__() + def __init__(self, config: ESMCConfig, layer_idx: int | None = None): + super().__init__(config, layer_idx) + # LayerNorm instead of Llama's RMSNorm. self.input_layernorm = nn.LayerNorm(config.hidden_size) - self.self_attn = ESMCAttention(config) self.post_attention_layernorm = nn.LayerNorm(config.hidden_size) - self.mlp = ESMCMLP(config) + # ESM3 residual scaling to stabilise deep networks. self.scaling_factor = math.sqrt(config.num_hidden_layers / 36) if config.scale_residue else 1.0 def forward( @@ -153,13 +225,9 @@ def forward( @auto_docstring -class ESMCPreTrainedModel(PreTrainedModel): +class ESMCPreTrainedModel(NomicBertPreTrainedModel): config_class = ESMCConfig base_model_prefix = "esmc" - supports_gradient_checkpointing = True - _supports_sdpa = True - _supports_flash_attn = True - _supports_attention_backend = True _no_split_modules = ["ESMCLayer"] _can_record_outputs = { "hidden_states": ESMCLayer, @@ -169,6 +237,9 @@ class ESMCPreTrainedModel(PreTrainedModel): # keys come from the published checkpoint's fused TransformerEngine layout. _keys_to_ignore_on_load_unexpected = [r"\._extra_state$", r"\.inv_freq$", r"\.original_inv_freq$"] + def _init_weights(self, module): + raise AttributeError() + @auto_docstring class ESMCModel(ESMCPreTrainedModel): @@ -178,8 +249,10 @@ def __init__(self, config: ESMCConfig): self.rotary_emb = ESMCRotaryEmbedding(config) self.layers = nn.ModuleList([ESMCLayer(config) for _ in range(config.num_hidden_layers)]) self.norm = nn.LayerNorm(config.hidden_size, bias=False) + self.gradient_checkpointing = False self.post_init() + @merge_with_config_defaults @capture_outputs @auto_docstring def forward( @@ -218,25 +291,20 @@ def forward( position_embeddings = self.rotary_emb(hidden_states, position_ids) if sequence_id is not None: - # Chain-aware attention: a token attends only to tokens sharing its - # ``sequence_id`` (block-diagonal), expressed as an additional ``and`` mask - # over the bidirectional mask. ``attention_mask`` is ignored here -- padding - # is encoded as ``sequence_id == -1``. Flash attention can't represent a - # block-diagonal mask without varlen, so multi-chain inputs are rejected. - if is_flash_attention_requested(self.config) and (sequence_id > 0).any(): + if is_flash_attention_requested(self.config): raise ValueError( - "Multi-chain ``sequence_id`` (any value > 0) is not supported with " - "flash attention. Re-load the model with attn_implementation='sdpa' " - "(or 'eager' / 'flex_attention') for chain-aware attention masking." + "`sequence_id` (chain-aware attention) is not supported with flash attention, " + "which can't represent the block-diagonal chain mask. Re-load the model with " + "attn_implementation='sdpa' (or 'eager' / 'flex_attention')." ) - attn_bias = create_bidirectional_mask( + attention_mask = create_bidirectional_mask( config=self.config, inputs_embeds=hidden_states, attention_mask=None, and_mask_function=packed_sequence_mask_function(sequence_id), ) else: - attn_bias = create_bidirectional_mask( + attention_mask = create_bidirectional_mask( config=self.config, inputs_embeds=hidden_states, attention_mask=attention_mask, @@ -245,7 +313,7 @@ def forward( for layer in self.layers: hidden_states = layer( hidden_states, - attn_bias, + attention_mask, position_embeddings=position_embeddings, **kwargs, ) @@ -364,6 +432,7 @@ class ESMCForTokenClassification(GenericForTokenClassification, ESMCPreTrainedMo __all__ = [ + "ESMCConfig", "ESMCModel", "ESMCForMaskedLM", "ESMCForSequenceClassification", diff --git a/tests/models/esmc/test_modeling_esmc.py b/tests/models/esmc/test_modeling_esmc.py index b1527091e2bd..bb099dd6245c 100644 --- a/tests/models/esmc/test_modeling_esmc.py +++ b/tests/models/esmc/test_modeling_esmc.py @@ -15,8 +15,8 @@ import unittest -from transformers import ESMCConfig, is_torch_available -from transformers.testing_utils import require_torch, slow, torch_device +from transformers import AutoTokenizer, ESMCConfig, is_torch_available +from transformers.testing_utils import Expectations, require_torch, slow, torch_device from ...test_configuration_common import ConfigTester from ...test_modeling_common import ModelTesterMixin, ids_tensor, random_attention_mask @@ -189,13 +189,63 @@ def test_for_token_classification(self): @slow @require_torch class ESMCModelIntegrationTest(unittest.TestCase): + checkpoint = "biohub/ESMC-300M" + sequence = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ" + def test_inference_masked_lm(self): - model = ESMCForMaskedLM.from_pretrained("biohub/ESMC-300M").to(torch_device).eval() - from transformers import AutoTokenizer + model = ESMCForMaskedLM.from_pretrained(self.checkpoint, dtype=torch.bfloat16).to(torch_device).eval() + tokenizer = AutoTokenizer.from_pretrained(self.checkpoint) + inputs = tokenizer([self.sequence], return_tensors="pt").to(torch_device) - tokenizer = AutoTokenizer.from_pretrained("biohub/ESMC-300M") - inputs = tokenizer(["MKTAYIAKQR"], return_tensors="pt").to(torch_device) with torch.no_grad(): logits = model(**inputs).logits + self.assertEqual(logits.shape, (1, inputs["input_ids"].shape[1], model.config.vocab_size)) self.assertTrue(torch.isfinite(logits).all()) + + # fmt: off + expected_slice = Expectations( + { + (None, None): torch.tensor([ + [-36.000, -36.000, -36.000, 14.250, 21.250, 20.125], + [-29.750, -29.750, -29.875, 22.500, 28.125, 27.750], + [-31.250, -31.250, -31.250, 21.250, 27.500, 27.125], + ]), + ("cpu", None): torch.tensor([ + [-36.000, -36.000, -36.250, 14.250, 21.375, 20.125], + [-29.875, -29.875, -29.875, 22.500, 28.125, 27.750], + [-31.250, -31.250, -31.250, 21.125, 27.500, 27.125], + ]), + } + ).get_expectation() + # fmt: on + torch.testing.assert_close(logits[0, 1:4, :6].float().cpu(), expected_slice, rtol=1e-2, atol=0.5) + + def test_inference_last_hidden_state(self): + model = ESMCModel.from_pretrained(self.checkpoint, dtype=torch.bfloat16).to(torch_device).eval() + tokenizer = AutoTokenizer.from_pretrained(self.checkpoint) + inputs = tokenizer([self.sequence], return_tensors="pt").to(torch_device) + + with torch.no_grad(): + last_hidden_state = model(**inputs).last_hidden_state + + self.assertEqual(last_hidden_state.shape, (1, inputs["input_ids"].shape[1], model.config.hidden_size)) + self.assertTrue(torch.isfinite(last_hidden_state).all()) + + # fmt: off + expected_slice = Expectations( + { + (None, None): torch.tensor([ + [ 0.006805, -0.008179, 0.038574, 0.038330, 0.011841, 0.039307], + [-0.016113, -0.017090, 0.008972, 0.027832, 0.003937, 0.071777], + [-0.003204, -0.026367, 0.002411, 0.024170, 0.025024, 0.047852], + ]), + ("cpu", None): torch.tensor([ + [ 0.007080, -0.008179, 0.038574, 0.038574, 0.011597, 0.039307], + [-0.015991, -0.016968, 0.008911, 0.027710, 0.003784, 0.071777], + [-0.003159, -0.026489, 0.002502, 0.024170, 0.024902, 0.047852], + ]), + } + ).get_expectation() + # fmt: on + torch.testing.assert_close(last_hidden_state[0, 1:4, :6].float().cpu(), expected_slice, rtol=1e-2, atol=1e-2) diff --git a/tests/models/esmc/test_tokenization_esmc.py b/tests/models/esmc/test_tokenization_esmc.py index 1f5ecc21693b..d4e79f6a3e49 100644 --- a/tests/models/esmc/test_tokenization_esmc.py +++ b/tests/models/esmc/test_tokenization_esmc.py @@ -12,20 +12,41 @@ # See the License for the specific language governing permissions and # limitations under the License. -import tempfile import unittest from transformers import AutoTokenizer, ESMCTokenizer from transformers.testing_utils import require_tokenizers, slow +from ...test_tokenization_common import TokenizerTesterMixin + @require_tokenizers -class ESMCTokenizationTest(unittest.TestCase): +class ESMCTokenizationTest(TokenizerTesterMixin, unittest.TestCase): tokenizer_class = ESMCTokenizer + test_seq2seq = False + + @classmethod + def setUpClass(cls): + super().setUpClass() + # ESMC is a fast-only tokenizer with a fixed amino-acid vocab built in __init__ (no vocab + # file), so seed the shared tmpdir with a code-built tokenizer for the common-test battery. + ESMCTokenizer().save_pretrained(cls.tmpdirname) def get_tokenizer(self, **kwargs) -> ESMCTokenizer: - # ESMC is a fast-only tokenizer; the vocab is built in __init__ (no vocab file needed). - return ESMCTokenizer(**kwargs) + return ESMCTokenizer.from_pretrained(self.tmpdirname, **kwargs) + + def get_input_output_texts(self, tokenizer): + # The common harness space-joins vocab tokens, but ESMC has no space token (spaces map to + # ````) and decode re-joins residues with spaces, so round-trip checks need a contiguous + # amino-acid input whose decoded form is the space-separated residues. + seq = "MKTAYIAKQRLAGVS" + return seq, " ".join(seq) + + def test_maximum_encoding_length_pair_input(self): + self.skipTest(reason="ESMC is a single-sequence protein tokenizer; it has no sequence-pair template.") + + def test_tokenizer_store_full_signature(self): + self.skipTest(reason="`chain_break_token` is fixed by the amino-acid vocab, not a stored init kwarg.") def test_documented_example(self): tokenizer = self.get_tokenizer() @@ -56,13 +77,6 @@ def test_special_token_ids(self): self.assertEqual(tokenizer.bos_token_id, tokenizer.cls_token_id) self.assertEqual(tokenizer.vocab_size, 33) - def test_batch_padding(self): - tokenizer = self.get_tokenizer() - batch = tokenizer(["LAGVS", "WC"], padding=True) - self.assertListEqual(batch["input_ids"][0], [0, 4, 5, 6, 7, 8, 2]) - self.assertListEqual(batch["input_ids"][1], [0, 22, 23, 2, 1, 1, 1]) - self.assertListEqual(batch["attention_mask"][1], [1, 1, 1, 1, 0, 0, 0]) - def test_chain_break_token(self): tokenizer = self.get_tokenizer() self.assertEqual(tokenizer.chain_break_token, "|") @@ -79,20 +93,6 @@ def test_unknown_residue_maps_to_unk(self): # "J" is not a valid amino-acid token in the ESMC vocabulary. self.assertIn(tokenizer.unk_token_id, tokenizer("MKJT")["input_ids"]) - def test_decode_round_trip(self): - tokenizer = self.get_tokenizer() - seq = "MKTAYIAKQR" - decoded = tokenizer.decode(tokenizer(seq)["input_ids"], skip_special_tokens=True).replace(" ", "") - self.assertEqual(decoded, seq) - - def test_save_and_load(self): - tokenizer = self.get_tokenizer() - seq = "MKTAYIAKQR" - with tempfile.TemporaryDirectory() as tmpdir: - tokenizer.save_pretrained(tmpdir) - reloaded = ESMCTokenizer.from_pretrained(tmpdir) - self.assertListEqual(tokenizer(seq)["input_ids"], reloaded(seq)["input_ids"]) - @slow def test_tokenizer_integration(self): # The published checkpoint's tokenizer.json must match the code-built tokenizer, diff --git a/utils/check_config_attributes.py b/utils/check_config_attributes.py index 64bc43860f47..60fd851c1b41 100644 --- a/utils/check_config_attributes.py +++ b/utils/check_config_attributes.py @@ -63,6 +63,7 @@ "NougatConfig": ["decoder", "encoder"], "PI0Config": ["vlm_projection_dim"], "EuroBertConfig": ["is_causal"], # not used directly, allows causal-bidirectional switch + "ESMCConfig": ["expansion_ratio"], # consumed in __post_init__ to derive intermediate_size "Ernie4_5_VL_MoeConfig": ["args"], # BC Alias "Ernie4_5_VL_MoeTextConfig": ["args"], # BC Alias "Ernie4_5_VL_MoeVisionConfig": ["args"], # BC Alias From 425ba1b78f0c782c1402e525404bc6ce6c377787 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jun 2026 17:42:58 +0100 Subject: [PATCH 62/70] make fix-repo --- docs/source/en/model_doc/esmc.md | 2 +- docs/source/en/model_doc/esmfold2.md | 2 +- src/transformers/models/esmc/modeling_esmc.py | 8 -------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 532d9527a359..424ed3afbd34 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-25.* +*This model was contributed to Hugging Face Transformers on 2026-06-30.* # ESMC diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index 403bb23e0fe4..f968df2f89e3 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-25.* +*This model was contributed to Hugging Face Transformers on 2026-06-30.* # ESMFold2 diff --git a/src/transformers/models/esmc/modeling_esmc.py b/src/transformers/models/esmc/modeling_esmc.py index ab2720ebeccb..ee781ba3f3e9 100644 --- a/src/transformers/models/esmc/modeling_esmc.py +++ b/src/transformers/models/esmc/modeling_esmc.py @@ -357,14 +357,6 @@ def forward( position_embeddings = self.rotary_emb(hidden_states, position_ids) if sequence_id is not None: - # Chain-aware attention: a token attends only to tokens sharing its ``sequence_id`` - # (block-diagonal), expressed as an additional ``and`` mask over the bidirectional mask. - # ``attention_mask`` is ignored here -- padding is encoded as ``sequence_id == -1``. - # Flash attention can't represent this block-diagonal mask: it would be silently dropped, - # letting tokens attend across chains (and to padding). So ``sequence_id`` requires a - # non-flash backend. This is a config-only check (no data inspection), so it stays - # ``torch.compile`` / ``torch.export`` friendly -- unlike inspecting the data to decide, - # which would either break tracing or silently miss the drop under a captured graph. if is_flash_attention_requested(self.config): raise ValueError( "`sequence_id` (chain-aware attention) is not supported with flash attention, " From db59333bc016f48eb95ab2c8c642f8892ac1bc7e Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jun 2026 18:02:25 +0100 Subject: [PATCH 63/70] tokenizer fixup --- src/transformers/models/esmc/tokenization_esmc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transformers/models/esmc/tokenization_esmc.py b/src/transformers/models/esmc/tokenization_esmc.py index da2cafa8ef9f..d6b3ed08d82d 100644 --- a/src/transformers/models/esmc/tokenization_esmc.py +++ b/src/transformers/models/esmc/tokenization_esmc.py @@ -83,7 +83,8 @@ class ESMCTokenizer(TokenizersBackend): The mask token, used for masked language modelling. eos_token (`str`, *optional*, defaults to `""`): The end-of-sequence token (appended to every sequence). - bos_token (``, *optional*): + bos_token (`str`, *optional*, defaults to `""`): + The beginning-of-sequence token (prepended to every sequence). When unset, uses cls_token. chain_break_token (`str`, *optional*, defaults to `"|"`): Token inserted between chains in multi-chain protein inputs. From 852145b09cec5d1c85e73527a9adc15f2961aef9 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 1 Jul 2026 12:44:27 +0100 Subject: [PATCH 64/70] Remove FA2 for esmfold2, lift some constants into the config --- .../models/esmfold2/configuration_esmfold2.py | 12 ++ .../models/esmfold2/modeling_esmfold2.py | 201 +++++++----------- 2 files changed, 94 insertions(+), 119 deletions(-) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index af2ec821013f..cd7c1e3b9a78 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -206,6 +206,14 @@ class ESMFold2Config(PreTrainedConfig): Number of trunk loops for iterative refinement. num_diffusion_samples (`int`, *optional*, defaults to 8): Number of parallel structure predictions to generate. + num_res_types (`int`, *optional*, defaults to 33): + Number of residue types for the one-hot residue/MSA features. + max_atomic_number (`int`, *optional*, defaults to 128): + Size of the element (atomic-number) one-hot in the atom featurization. + char_vocab_size (`int`, *optional*, defaults to 64): + Vocabulary size for the per-character atom-name encoding. + max_chars (`int`, *optional*, defaults to 4): + Maximum number of characters per atom name. esmc_config (`ESMCConfig`, *optional*): Configuration of the bundled ESMC language-model backbone. Defaults to the ESMC-6B configuration. The backbone weights are part of the ESMFold2 @@ -263,6 +271,10 @@ class ESMFold2Config(PreTrainedConfig): n_relative_chain_bins: int | None = 2 num_loops: int | None = 10 num_diffusion_samples: int | None = 8 + num_res_types: int | None = 33 + max_atomic_number: int | None = 128 + char_vocab_size: int | None = 64 + max_chars: int | None = 4 esmc_config: dict | ESMCConfig | None = None msa_encoder_overwrite: bool | None = True inputs: dict | InputsEmbedderConfig | None = None diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 7d61bc0130d4..4d4e7474654d 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -25,19 +25,15 @@ from ... import initialization as init from ...integrations import use_kernel_forward_from_hub +from ...masking_utils import create_bidirectional_mask from ...modeling_outputs import ModelOutput from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel from ...processing_utils import Unpack -from ...utils import TransformersKwargs, is_flash_attn_2_available +from ...utils import TransformersKwargs from ..auto import AutoModel from .configuration_esmfold2 import ESMFold2Config -if is_flash_attn_2_available(): - from flash_attn import flash_attn_func, flash_attn_varlen_func - from flash_attn.bert_padding import index_first_axis, pad_input - - # =========================================================================== # fp32-compute LayerNorm # =========================================================================== @@ -290,17 +286,30 @@ def _resolve_atom_config(config: ESMFold2Config, structure_prediction: bool): # =========================================================================== +def _swa_window_mask_function(rank: Tensor, valid: Tensor, half_window: int) -> Callable: + """Sliding-window ``and`` mask over the valid-token rank, with self-attention always allowed. + + ``rank[b, i]`` is the cumulative index of position ``i`` among valid (non-padding) tokens, so + padding does not count toward the window distance: a token attends to another iff both are + valid and their ranks differ by at most ``half_window`` (or it is the diagonal). + """ + + def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool: + within = (rank[batch_idx, q_idx] - rank[batch_idx, kv_idx]).abs() <= half_window + in_window = within & valid[batch_idx, q_idx] & valid[batch_idx, kv_idx] + return in_window | (q_idx == kv_idx) + + return inner_mask + + class ESMFold2SWA3DRoPEAttention(nn.Module): """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention - interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), - with the sliding window expressed as an additive attention mask. The custom - flash-attention path (native bidirectional ``window_size``, plus varlen for - packed inputs) is kept as an opt-in backend, selected when - ``_attn_implementation == "flash_attention_2"``. The shared ``config`` is - passed in at construction, so ``set_attn_implementation()`` stays live (it - mutates the same object); dims/window are derived from + interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), with + the sliding window expressed as an additive attention mask. The shared + ``config`` is passed in at construction, so ``set_attn_implementation()`` stays + live (it mutates the same object); dims/window are derived from ``(config, structure_prediction)`` via :func:`_resolve_atom_config`. """ @@ -340,65 +349,36 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: q, k, v = q.bfloat16(), k.bfloat16(), v.bfloat16() attn_impl = self.config._attn_implementation - use_flash = attn_impl == "flash_attention_2" and is_flash_attn_2_available() - if use_flash and len(attention_params) > 2: - indices, cu_seqlens, max_seqlen = ( - attention_params[2], - attention_params[3], - attention_params[4], - ) - q_unpad = index_first_axis(q.reshape(-1, self.n_heads, self.head_dim), indices) - k_unpad = index_first_axis(k.reshape(-1, self.n_heads, self.head_dim), indices) - v_unpad = index_first_axis(v.reshape(-1, self.n_heads, self.head_dim), indices) - out_unpad = flash_attn_varlen_func( - q_unpad, - k_unpad, - v_unpad, - cu_seqlens, - cu_seqlens, - max_seqlen, - max_seqlen, - softmax_scale=self.scale, - window_size=(self.half_window, self.half_window), - ) - out = pad_input(out_unpad, indices, B, N) - elif use_flash: - out = flash_attn_func( - q, - k, - v, - softmax_scale=self.scale, - window_size=(self.half_window, self.half_window), - ) + if len(attention_params) > 2: + valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) + valid[attention_params[2]] = True + valid = valid.view(B, N) else: - if len(attention_params) > 2: - valid = torch.zeros(B * N, dtype=torch.bool, device=q.device) - valid[attention_params[2]] = True - valid = valid.view(B, N) - else: - valid = torch.ones(B, N, dtype=torch.bool, device=q.device) - rank = torch.cumsum(valid, dim=1) - 1 - within = (rank.unsqueeze(2) - rank.unsqueeze(1)).abs() <= self.half_window - allowed = within & valid.unsqueeze(1) & valid.unsqueeze(2) - allowed |= torch.eye(N, dtype=torch.bool, device=q.device) - # Sliding window as an additive bias: 0 where allowed, -inf elsewhere. - attn_mask = torch.zeros(B, 1, N, N, dtype=q.dtype, device=q.device) - attn_mask = attn_mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(q.dtype).min) - - attention_interface: Callable = eager_attention_forward - if attn_impl != "eager": - attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) - out, _ = attention_interface( - self, - q.transpose(1, 2), - k.transpose(1, 2), - v.transpose(1, 2), - attn_mask, - dropout=0.0, - scaling=self.scale, - ) - out = out * valid.unsqueeze(-1).unsqueeze(-1) + valid = torch.ones(B, N, dtype=torch.bool, device=q.device) + # Sliding window over the valid-token rank, built through the masking framework as a + # backend-appropriate mask (bit-identical to a hand-built additive bias). + rank = torch.cumsum(valid, dim=1) - 1 + attn_mask = create_bidirectional_mask( + config=self.config, + inputs_embeds=q.reshape(B, N, -1), + attention_mask=None, + and_mask_function=_swa_window_mask_function(rank, valid, self.half_window), + ) + + attention_interface: Callable = eager_attention_forward + if attn_impl != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(attn_impl, eager_attention_forward) + out, _ = attention_interface( + self, + q.transpose(1, 2), + k.transpose(1, 2), + v.transpose(1, 2), + attn_mask, + dropout=0.0, + scaling=self.scale, + ) + out = out * valid.unsqueeze(-1).unsqueeze(-1) out = out.to(input_dtype).reshape(B, N, -1) out = out * torch.sigmoid(self.gate_proj(x_input)) @@ -414,10 +394,6 @@ def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift -def _gated_residual(x: Tensor, gate: Tensor, y: Tensor) -> Tensor: - return x + gate * y - - class ESMFold2SWAAtomBlock(nn.Module): """adaLN-Zero + SWA attention + ESMFold2SwiGLU FFN. @@ -441,11 +417,11 @@ def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: attn_input = _rms_adaln(x, scale_a, shift_a) attn_out = self.attn(attn_input, attention_params) - x = _gated_residual(x, gate_a, attn_out) + x = x + gate_a * attn_out ffn_input = _rms_adaln(x, scale_f, shift_f) ffn_out = self.ffn(ffn_input) - x = _gated_residual(x, gate_f, ffn_out) + x = x + gate_f * ffn_out return x @@ -529,18 +505,6 @@ def forward( return q_l -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- -CHAR_VOCAB_SIZE = 64 -MAX_CHARS = 4 -XYZ_DIMS = 3 -MAX_ATOMIC_NUMBER = 128 - -# Input feature dim = 3 + 1 + 1 + 128 + 64*4 = 389 -ATOM_FEATURE_DIM = XYZ_DIMS + 1 + 1 + MAX_ATOMIC_NUMBER + CHAR_VOCAB_SIZE * MAX_CHARS - - def scatter_atom_to_token( atom_features: Tensor, atom_to_token_idx: Tensor, @@ -591,7 +555,10 @@ def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> self.d_token = d_token self.structure_prediction = structure_prediction - self.atom_linear = nn.Linear(ATOM_FEATURE_DIM, d_atom, bias=False) + # Atom feature width = 3 (xyz) + 1 (charge) + 1 (mask) + element + atom-name-char one-hots. + self.char_feature_dim = config.char_vocab_size * config.max_chars + atom_feature_dim = 3 + 1 + 1 + config.max_atomic_number + self.char_feature_dim + self.atom_linear = nn.Linear(atom_feature_dim, d_atom, bias=False) self.atom_norm = ESMFold2LayerNorm(d_atom) if structure_prediction: @@ -635,7 +602,7 @@ def forward( ref_charge.unsqueeze(-1), atom_attention_mask.unsqueeze(-1), ref_element, - ref_atom_name_chars.reshape(B, N, MAX_CHARS * CHAR_VOCAB_SIZE), + ref_atom_name_chars.reshape(B, N, self.char_feature_dim), ], dim=-1, ) @@ -646,11 +613,8 @@ def forward( cos = cos.repeat_interleave(num_diffusion_samples, 0) sin = sin.repeat_interleave(num_diffusion_samples, 0) mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) - seqlens = mask_exp.sum(dim=-1, dtype=torch.int32) indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() - max_seqlen = int(seqlens.max().item()) - cu_seqlens = F.pad(torch.cumsum(seqlens, dim=0, dtype=torch.int32), (1, 0)) - attention_params = (cos, sin, indices, cu_seqlens, max_seqlen) + attention_params = (cos, sin, indices) n_tokens = int(atom_to_token.max().item()) + 1 if layer_cache is not None: layer_cache["c_base"] = c_base @@ -699,6 +663,12 @@ def forward( # =========================================================================== +def _gather_along_dim1(source: Tensor, index: Tensor) -> Tensor: + """Gather ``source`` (``[B, N, d]``) along dim 1 with a ``[B, M]`` index, returning ``[B, M, d]``.""" + idx = index.unsqueeze(-1).expand(-1, -1, source.size(-1)) + return torch.gather(source, 1, idx) + + def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> Tensor: """Broadcast per-token features to per-atom features using gather. @@ -709,8 +679,7 @@ def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> T Returns: [B, A, d] """ - idx = atom_to_token_idx.unsqueeze(-1).expand(-1, -1, token_features.size(-1)) - return torch.gather(token_features, 1, idx) + return _gather_along_dim1(token_features, atom_to_token_idx) # =========================================================================== @@ -733,7 +702,7 @@ def __init__(self, config: ESMFold2Config) -> None: self.atom_transformer = ESMFold2SWAAtomTransformer(config, structure_prediction=True) self.norm = ESMFold2LayerNorm(d_atom) - self.output_linear = nn.Linear(d_atom, XYZ_DIMS, bias=False) + self.output_linear = nn.Linear(d_atom, 3, bias=False) # (x, y, z) coordinates def forward( self, @@ -1418,7 +1387,7 @@ def forward(self, z: Tensor, mask: Tensor) -> Tensor: mask_bias = torch.where( mask[:, None, :].bool(), torch.zeros_like(scores), - torch.full_like(scores, -1e9), + torch.full_like(scores, torch.finfo(scores.dtype).min), ) scores = scores + mask_bias weights = F.softmax(scores, dim=-1, dtype=torch.float32).to(scores.dtype) @@ -1711,9 +1680,9 @@ def forward(self, pair_grid: Tensor, visibility: Tensor | None = None) -> Tensor class ESMFold2TriangleMultiplicativeUpdate(nn.Module): """Thin wrapper exposing the triangular mixer with explicit orientation (v3).""" - def __init__(self, dim: int = 128, _outgoing: bool = True) -> None: + def __init__(self, dim: int = 128, outgoing: bool = True) -> None: super().__init__() - flow = "outgoing" if _outgoing else "incoming" + flow = "outgoing" if outgoing else "incoming" self._engine = ESMFold2TriangleMultiplicativeBlock(input_channels=dim, latent_channels=dim, flow=flow) def set_chunk_size(self, chunk_size: int | None) -> None: @@ -1757,8 +1726,8 @@ class ESMFold2PairUpdateBlock(nn.Module): def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: super().__init__() - self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=True) + self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=False) self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=expansion_ratio) def set_chunk_size(self, chunk_size: int | None) -> None: @@ -1965,8 +1934,7 @@ def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: Returns: [B, L, 3] """ - idx = rep_atom_idx.unsqueeze(-1).expand(-1, -1, coords.size(-1)) - return torch.gather(coords, 1, idx) + return _gather_along_dim1(coords, rep_atom_idx) def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: @@ -2009,7 +1977,6 @@ def _categorical_mean(logits: Tensor, start: float, end: float) -> Tensor: _CONFIDENCE_EPS = 1e-6 -_NONPOLYMER_ID = 4 class ESMFold2ConfidenceHead(nn.Module): @@ -2147,7 +2114,7 @@ def forward( expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) - is_ligand = (expanded_type == _NONPOLYMER_ID).float() + is_ligand = (expanded_type == 4).float() # 4 = non-polymer (ligand) molecule type inter_chain = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() near_contact = (rep_distances < 8).float() interface_per_token = (near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1)).amax(dim=-1) @@ -2247,7 +2214,6 @@ class ESMFold2PreTrainedModel(PreTrainedModel): # precision drives the diffusion conditioning; keep them fp32 even under dtype=bf16. _keep_in_fp32_modules_strict = ["fourier"] _supports_sdpa = True - _supports_flash_attn = True _supports_attention_backend = True def _init_weights(self, module): @@ -2282,9 +2248,6 @@ def _init_weights(self, module): init.zeros_(module.base_z_combine) -NUM_RES_TYPES = 33 - - # =========================================================================== # ESMFold2 — language-model backbone helpers # =========================================================================== @@ -2609,13 +2572,13 @@ def forward( total_steps = max(1, n_loops + 1) if res_type.dim() == 2: - res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float() + res_type_oh = F.one_hot(res_type.long(), num_classes=self.config.num_res_types).float() res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() else: res_type_oh = res_type.float() if msa is not None: - msa_oh_profile = F.one_hot(msa.long(), num_classes=NUM_RES_TYPES).float() + msa_oh_profile = F.one_hot(msa.long(), num_classes=self.config.num_res_types).float() if msa_attention_mask is not None: mask_f = msa_attention_mask.float().unsqueeze(-1) msa_oh_profile = msa_oh_profile * mask_f @@ -2629,8 +2592,8 @@ def forward( if deletion_mean is None: deletion_mean = torch.zeros(res_type.shape[0], res_type.shape[1], device=res_type.device) - ref_element_oh = F.one_hot(ref_element.long(), num_classes=MAX_ATOMIC_NUMBER).float() - ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE).float() + ref_element_oh = F.one_hot(ref_element.long(), num_classes=self.config.max_atomic_number).float() + ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=self.config.char_vocab_size).float() # Bias-free downstream Linears require zeroed padding. atm_mask_f = atm_mask.float() ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) @@ -2680,7 +2643,7 @@ def forward( _msa_kwargs: dict | None = None if self.msa_encoder is not None and msa is not None: B_msa, M, L_msa = msa.shape - msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES).float() + msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=self.config.num_res_types).float() msa_attn = ( msa_attention_mask.permute(0, 2, 1).float() if msa_attention_mask is not None @@ -2813,8 +2776,8 @@ def __init__(self, config: ESMFold2Config, is_final_block: bool = False) -> None d_msa, d_pair, n_heads_msa, msa_head_width ) self.msa_transition = ESMFold2Transition(d_msa, expansion_ratio=4) - self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True) - self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False) + self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=True) + self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=False) self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=4) def set_chunk_size(self, chunk_size: int | None) -> None: @@ -2851,8 +2814,8 @@ def __init__(self, config: ESMFold2Config) -> None: d_msa = me.d_msa d_inputs = config.inputs.d_inputs n_layers = me.n_layers - # 35 = NUM_RES_TYPES (33) one-hot + has_deletion + deletion_value. - self.embed = nn.Linear(35, d_msa, bias=False) + # num_res_types one-hot + has_deletion + deletion_value. + self.embed = nn.Linear(config.num_res_types + 2, d_msa, bias=False) self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False) self.blocks = nn.ModuleList( [ESMFold2MSAEncoderBlock(config, is_final_block=(i == n_layers - 1)) for i in range(n_layers)] From 7612e479cc28f280d44a3d6613a3fc4fa580a20c Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 1 Jul 2026 18:03:38 +0100 Subject: [PATCH 65/70] Big refactor to address reviewer comments --- src/transformers/conversion_mapping.py | 29 + .../models/esmfold2/configuration_esmfold2.py | 57 +- .../models/esmfold2/modeling_esmfold2.py | 1178 ++++++++--------- 3 files changed, 642 insertions(+), 622 deletions(-) diff --git a/src/transformers/conversion_mapping.py b/src/transformers/conversion_mapping.py index 81a7e927b879..8365b569296e 100755 --- a/src/transformers/conversion_mapping.py +++ b/src/transformers/conversion_mapping.py @@ -828,6 +828,35 @@ def _build_checkpoint_conversion_mapping(): WeightRenaming(r"lm_head\.2\.", "lm_head.layer_norm."), WeightRenaming(r"lm_head\.3\.", "lm_head.decoder."), ], + "esmfold2": [ + # TODO(temporary): the published ESMFold2 checkpoint predates the SwiGLU consolidation + # and still stores those blocks under their old per-module names. Drop this whole entry + # once the merged ESMFold2+ESMC checkpoint is regenerated with the canonical `w12`/`w3`. + WeightRenaming(r"\.w_up\.", ".w12."), # SwiGLU-FFN (atom transformer) + WeightRenaming(r"\.w_down\.", ".w3."), + WeightRenaming(r"\.lin_swish\.", ".ffn.w12."), # ConditionedTransitionBlock + WeightRenaming(r"\.lin_out\.", ".ffn.w3."), + # TODO(temporary): checkpoint predates the SWA attention q/k/v projection split (M19); + # it packed q/k/v into a single ``Wqkv``. Drop once the merged checkpoint is regenerated + # with split projections. (The pair-bias attention keeps its packed ``kv_proj`` for now: + # splitting it too would clash with these q/k/v names under the bidirectional converter.) + WeightConverter( + source_patterns=["attn.Wqkv.weight"], + target_patterns=["attn.q_proj.weight", "attn.k_proj.weight", "attn.v_proj.weight"], + operations=[Chunk(dim=0)], + ), + # TODO(temporary): checkpoint predates de-Sequentializing the nn.Sequential blocks (M26) + # into named submodules. Drop this whole group after the merged checkpoint is regenerated. + WeightRenaming(r"output_mlp\.0\.", "output_fc1."), # SingleToPair (Linear, GELU, Linear) + WeightRenaming(r"output_mlp\.2\.", "output_fc2."), + WeightRenaming(r"adaln_modulation\.1\.", "adaln_linear."), # SWAAtomBlock (SiLU, Linear) + WeightRenaming(r"base_z_linear\.0\.", "base_z_input_norm."), # LM shim (Norm, Linear) + WeightRenaming(r"base_z_linear\.1\.", "base_z_proj."), + WeightRenaming(r"base_z_mlp\.0\.", "base_z_to_pair."), # LM shim (SingleToPair, Norm) + WeightRenaming(r"base_z_mlp\.1\.", "base_z_output_norm."), + WeightRenaming(r"compute_bias\.0\.", "bias_norm."), # MSAPairWeightedAveraging (Norm, Linear) + WeightRenaming(r"compute_bias\.1\.", "bias_proj."), + ], "dinov3_convnext": [WeightRenaming(r"(? Tensor: return F.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).to(x.dtype) -# =========================================================================== -# ESMFold2TransitionLayer (used in ESMFold2DiffusionConditioning) -# =========================================================================== - - class ESMFold2TransitionLayer(nn.Module): - """ESMFold2SwiGLU transition: norm -> a_proj, b_proj -> silu(a)*b -> out_proj.""" + """ESMFold2SwiGLU transition: norm -> a_proj/b_proj -> silu(gate) * up -> out_proj.""" def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: super().__init__() @@ -70,14 +60,9 @@ def __init__(self, d_model: int, n: int, eps: float = 1e-5) -> None: def forward(self, x: Tensor) -> Tensor: x = self.norm(x) - a = self.a_proj(x) - b = self.b_proj(x) - return self.out_proj(F.silu(a) * b) - - -# =========================================================================== -# ESMFold2AdaptiveLayerNorm (used in ESMFold2DiffusionTransformer) -# =========================================================================== + gate = self.a_proj(x) + up = self.b_proj(x) + return self.out_proj(F.silu(gate) * up) class ESMFold2AdaptiveLayerNorm(nn.Module): @@ -102,11 +87,6 @@ def forward(self, a: Tensor, s: Tensor) -> Tensor: return (gate * a_norm + shift).to(a.dtype) -# =========================================================================== -# ESMFold2FourierEmbedding -# =========================================================================== - - class ESMFold2FourierEmbedding(nn.Module): """Fourier embedding: cos(2*pi*(t*w + b)).""" @@ -126,11 +106,6 @@ def forward(self, t_hat: Tensor) -> Tensor: return torch.cos(2.0 * torch.pi * (t[:, None] * self.w[None, :] + self.b[None, :])) -# =========================================================================== -# ESMFold2SwiGLU / ESMFold2SwiGLUMLP -# =========================================================================== - - class ESMFold2SwiGLU(nn.Module): """ESMFold2SwiGLU with packed w12 and output w3.""" @@ -148,9 +123,9 @@ def __init__( self.hidden_features = hidden_features def forward(self, x: Tensor) -> Tensor: - x12 = self.w12(x) - x1, x2 = x12.split(self.hidden_features, dim=-1) - hidden = F.silu(x1) * x2 + gate_up = self.w12(x) + gate, up = gate_up.split(self.hidden_features, dim=-1) + hidden = F.silu(gate) * up return self.w3(hidden) @@ -162,24 +137,12 @@ def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) - super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) -# =========================================================================== -# ESMFold2SwiGLUFFN (atom transformer blocks) -# =========================================================================== - - -class ESMFold2SwiGLUFFN(nn.Module): +class ESMFold2SwiGLUFFN(ESMFold2SwiGLU): """ESMFold2SwiGLU FFN with rounded hidden size for hardware alignment.""" def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: - super().__init__() hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 - self.w_up = nn.Linear(d_model, 2 * hidden_size, bias=False) - self.w_down = nn.Linear(hidden_size, d_model, bias=False) - - def forward(self, x: Tensor) -> Tensor: - x = x.to(self.w_up.weight.dtype) - x1, x2 = self.w_up(x).chunk(2, dim=-1) - return self.w_down(F.silu(x1) * x2) + super().__init__(in_features=d_model, hidden_features=hidden_size, out_features=d_model, bias=False) # Copied from transformers.models.llama.modeling_llama.rotate_half @@ -229,26 +192,17 @@ def eager_attention_forward( return attn_output, attn_weights -# =========================================================================== -# SWA Atom Attention components -# =========================================================================== - - def apply_rotary_emb_3d(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: - """Apply RoPE with batch-dependent cos/sin. + """Apply 3D RoPE with batch-dependent cos/sin. Args: x: [B, L, H, D] - cos: [B, L, D/2] - sin: [B, L, D/2] + cos: [B, L, D] (full head dim, built by ``ESMFold2RotaryEmbedding3D``) + sin: [B, L, D] """ - ro_dim = cos.shape[-1] * 2 - cos = cos.unsqueeze(2).repeat(1, 1, 1, 2) - sin = sin.unsqueeze(2).repeat(1, 1, 1, 2) - return torch.cat( - [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim]) * sin, x[..., ro_dim:]], - dim=-1, - ) + cos = cos.unsqueeze(2) + sin = sin.unsqueeze(2) + return x * cos + rotate_half(x) * sin def qk_norm(x: Tensor) -> Tensor: @@ -277,13 +231,8 @@ def _resolve_atom_config(config: ESMFold2Config, structure_prediction: bool): swa = config.inputs.atom_encoder if structure_prediction: dm = config.structure_head.diffusion_module - return dm.c_atom, dm.c_token, dm.atom_num_blocks, dm.atom_num_heads, swa - return swa.d_atom, swa.d_token, swa.n_blocks, swa.n_heads, swa - - -# =========================================================================== -# ESMFold2SWA3DRoPEAttention -# =========================================================================== + return dm.atom_hidden_size, dm.token_hidden_size, dm.atom_num_blocks, dm.atom_num_heads, swa + return swa.atom_hidden_size, swa.token_hidden_size, swa.n_blocks, swa.n_heads, swa def _swa_window_mask_function(rank: Tensor, valid: Tensor, half_window: int) -> Callable: @@ -303,7 +252,7 @@ def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool: class ESMFold2SWA3DRoPEAttention(nn.Module): - """Sliding window self-attention with 3D RoPE. Has Wqkv, gate_proj, out_proj. + """Sliding window self-attention with 3D RoPE. Has q/k/v/gate/out projections. The plain ``softmax(QKᵀ)V`` core is dispatched through the v5 attention interface (``config._attn_implementation``: ``eager`` / ``sdpa`` / ...), with @@ -327,7 +276,9 @@ def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> # causal masking when attention_mask happens to be None. self.is_causal = False - self.Wqkv = nn.Linear(d_model, 3 * d_model, bias=False) + self.q_proj = nn.Linear(d_model, d_model, bias=False) + self.k_proj = nn.Linear(d_model, d_model, bias=False) + self.v_proj = nn.Linear(d_model, d_model, bias=False) self.out_proj = nn.Linear(d_model, d_model, bias=False) self.gate_proj = nn.Linear(d_model, d_model, bias=False) @@ -336,9 +287,9 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: cos, sin = attention_params[0], attention_params[1] x_input = x - qkv = self.Wqkv(x) - qkv = qkv.view(B, N, 3, self.n_heads, self.head_dim).permute(2, 0, 1, 3, 4) - q, k, v = qkv.unbind(0) + q = self.q_proj(x).view(B, N, self.n_heads, self.head_dim) + k = self.k_proj(x).view(B, N, self.n_heads, self.head_dim) + v = self.v_proj(x).view(B, N, self.n_heads, self.head_dim) q, k = qk_norm(q), qk_norm(k) q = apply_rotary_emb_3d(q, cos, sin) @@ -385,11 +336,6 @@ def forward(self, x: Tensor, attention_params: tuple) -> Tensor: return self.out_proj(out) -# =========================================================================== -# ESMFold2SWAAtomBlock, ESMFold2SWAAtomTransformer -# =========================================================================== - - def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: return F.rms_norm(x, (x.shape[-1],)) * (1 + scale) + shift @@ -397,23 +343,23 @@ def _rms_adaln(x: Tensor, scale: Tensor, shift: Tensor) -> Tensor: class ESMFold2SWAAtomBlock(nn.Module): """adaLN-Zero + SWA attention + ESMFold2SwiGLU FFN. - Creates adaln_modulation = Sequential(SiLU(), Linear) -> keys like adaln_modulation.1.weight + The adaLN-Zero modulation is ``adaln_linear`` applied to ``silu(atom_cond)`` (zero-init gate). """ def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() d_atom, _d_token, _n_blocks, _n_heads, swa = _resolve_atom_config(config, structure_prediction) # adaln-Zero gate; zero-init lives in ESMFold2PreTrainedModel._init_weights. - self.adaln_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_atom, 6 * d_atom, bias=False)) + self.adaln_linear = nn.Linear(d_atom, 6 * d_atom, bias=False) self.attn = ESMFold2SWA3DRoPEAttention(config, structure_prediction) self.ffn = ESMFold2SwiGLUFFN(d_atom, swa.expansion_ratio) - def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: - mod = self.adaln_modulation(c_l) - if mod.dim() == 2: - mod = mod.unsqueeze(1) - shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod.chunk(6, dim=-1) + def forward(self, x: Tensor, atom_cond: Tensor, attention_params: tuple) -> Tensor: + modulation = self.adaln_linear(F.silu(atom_cond)) + if modulation.dim() == 2: + modulation = modulation.unsqueeze(1) + shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = modulation.chunk(6, dim=-1) attn_input = _rms_adaln(x, scale_a, shift_a) attn_out = self.attn(attn_input, attention_params) @@ -425,47 +371,51 @@ def forward(self, x: Tensor, c_l: Tensor, attention_params: tuple) -> Tensor: return x -@torch.compiler.disable -def build_3d_rope( - ref_pos: Tensor, - ref_space_uid: Tensor, - head_dim: int, - n_spatial_per_axis: int = 4, - n_uid_pairs: int = 2, - spatial_base_freq: float = 10000.0, - uid_base_freq: float = 10.0, -) -> tuple[Tensor, Tensor]: - """Build cos/sin for 3D RoPE + UID RoPE.""" - device = ref_pos.device - B, N = ref_pos.shape[:2] - half_dim = head_dim // 2 - n_spatial_total = 3 * n_spatial_per_axis - - spatial_inv_freq = 1.0 / ( - spatial_base_freq - ** (torch.arange(0, n_spatial_per_axis, dtype=torch.float32, device=device) / n_spatial_per_axis) - ) - uid_inv_freq = 1.0 / ( - uid_base_freq ** (torch.arange(0, n_uid_pairs, dtype=torch.float32, device=device) / n_uid_pairs) - ) +class ESMFold2RotaryEmbedding3D(nn.Module): + """Rotary embedding over continuous 3D atom coordinates plus a discrete space UID. + + Unlike sequence RoPE, this encodes physical position (``ref_pos`` = x/y/z) and a + per-atom space UID rather than token indices, with separate spatial and UID base + frequencies. ``forward`` returns cos/sin already at the full head dim, so the caller + applies plain rotate-half RoPE (:func:`apply_rotary_emb_3d`). The inverse frequencies + are cheap to rebuild each call and are computed on the input device to stay bit-exact. + """ + + def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: + super().__init__() + d_atom, _d_token, _n_blocks, n_heads, swa = _resolve_atom_config(config, structure_prediction) + self.head_dim = d_atom // n_heads + self.n_spatial_per_axis = swa.n_spatial_rope_pairs_per_axis + self.n_uid_pairs = swa.n_uid_rope_pairs + self.spatial_base_freq = swa.spatial_rope_base_frequency + self.uid_base_freq = swa.uid_rope_base_frequency - pos_f32 = ref_pos.float() - spatial_freqs = pos_f32.unsqueeze(-1) * spatial_inv_freq - spatial_freqs = spatial_freqs.reshape(B, N, n_spatial_total) + def forward(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: + device = ref_pos.device + B, N = ref_pos.shape[:2] + half_dim = self.head_dim // 2 + n_spatial_total = 3 * self.n_spatial_per_axis - uid_f32 = ref_space_uid.float() - uid_freqs = uid_f32.unsqueeze(-1) * uid_inv_freq + spatial_inv_freq = 1.0 / ( + self.spatial_base_freq + ** (torch.arange(0, self.n_spatial_per_axis, dtype=torch.float32, device=device) / self.n_spatial_per_axis) + ) + uid_inv_freq = 1.0 / ( + self.uid_base_freq + ** (torch.arange(0, self.n_uid_pairs, dtype=torch.float32, device=device) / self.n_uid_pairs) + ) - n_active = n_spatial_total + n_uid_pairs - freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) + spatial_freqs = (ref_pos.float().unsqueeze(-1) * spatial_inv_freq).reshape(B, N, n_spatial_total) + uid_freqs = ref_space_uid.float().unsqueeze(-1) * uid_inv_freq - if n_active < half_dim: - padding = torch.zeros(B, N, half_dim - n_active, device=device, dtype=torch.float32) - freqs = torch.cat([freqs, padding], dim=-1) + freqs = torch.cat([spatial_freqs, uid_freqs], dim=-1) + n_active = n_spatial_total + self.n_uid_pairs + if n_active < half_dim: + freqs = torch.cat([freqs, freqs.new_zeros(B, N, half_dim - n_active)], dim=-1) - cos = freqs.cos().to(torch.bfloat16) - sin = freqs.sin().to(torch.bfloat16) - return cos, sin + # Duplicate to the full head dim so the caller applies standard rotate-half RoPE. + emb = torch.cat([freqs, freqs], dim=-1) + return emb.cos().to(torch.bfloat16), emb.sin().to(torch.bfloat16) class ESMFold2SWAAtomTransformer(nn.Module): @@ -473,36 +423,21 @@ class ESMFold2SWAAtomTransformer(nn.Module): def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - d_atom, _d_token, n_blocks, n_heads, swa = _resolve_atom_config(config, structure_prediction) + _d_atom, _d_token, n_blocks, _n_heads, swa = _resolve_atom_config(config, structure_prediction) self.swa_window_size = swa.swa_window_size - self.head_dim = d_atom // n_heads - self.spatial_rope_base_frequency = swa.spatial_rope_base_frequency - self.n_spatial_rope_pairs_per_axis = swa.n_spatial_rope_pairs_per_axis - self.n_uid_rope_pairs = swa.n_uid_rope_pairs - self.uid_rope_base_frequency = swa.uid_rope_base_frequency + self.rotary_emb = ESMFold2RotaryEmbedding3D(config, structure_prediction) self.blocks = nn.ModuleList([ESMFold2SWAAtomBlock(config, structure_prediction) for _ in range(n_blocks)]) - def _build_3d_rope(self, ref_pos: Tensor, ref_space_uid: Tensor) -> tuple[Tensor, Tensor]: - return build_3d_rope( - ref_pos=ref_pos, - ref_space_uid=ref_space_uid, - head_dim=self.head_dim, - n_spatial_per_axis=self.n_spatial_rope_pairs_per_axis, - n_uid_pairs=self.n_uid_rope_pairs, - spatial_base_freq=self.spatial_rope_base_frequency, - uid_base_freq=self.uid_rope_base_frequency, - ) - def forward( self, - q_l: Tensor, - c_l: Tensor, + atom_queries: Tensor, + atom_cond: Tensor, attention_params: tuple, ) -> Tensor: for block in self.blocks: - q_l = block(q_l, c_l, attention_params) - return q_l + atom_queries = block(atom_queries, atom_cond, attention_params) + return atom_queries def scatter_atom_to_token( @@ -534,11 +469,6 @@ def scatter_atom_to_token( return out[:, :n_tokens, :] -# =========================================================================== -# ESMFold2AtomEncoder (for both inputs_embedder and diffusion_module) -# =========================================================================== - - class ESMFold2AtomEncoder(nn.Module): """SWA atom encoder with atom_linear, atom_norm, atom_to_token_linear, [coords_linear], atom_transformer. @@ -570,6 +500,42 @@ def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> out_dim = d_token if structure_prediction else d_token // 2 self.atom_to_token_linear = nn.Linear(d_atom, out_dim, bias=False) + def _compute_step_invariants( + self, + ref_pos: Tensor, + atom_attention_mask: Tensor, + ref_space_uid: Tensor, + ref_charge: Tensor, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_to_token: Tensor, + num_diffusion_samples: int, + ) -> tuple[Tensor, tuple[Tensor, Tensor, Tensor], Tensor, int]: + """Tensors that don't change across diffusion steps (cached per fold): the atom base + embedding ``c_base``, 3D-RoPE ``(cos, sin, indices)``, the expanded atom mask, and n_tokens.""" + B, N = ref_pos.shape[:2] + atom_feats = torch.cat( + [ + ref_pos, + ref_charge.unsqueeze(-1), + atom_attention_mask.unsqueeze(-1), + ref_element, + ref_atom_name_chars.reshape(B, N, self.char_feature_dim), + ], + dim=-1, + ) + c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( + self.atom_linear.weight.dtype + ) + cos, sin = self.atom_transformer.rotary_emb(ref_pos, ref_space_uid) + cos = cos.repeat_interleave(num_diffusion_samples, 0) + sin = sin.repeat_interleave(num_diffusion_samples, 0) + mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) + indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() + attention_params = (cos, sin, indices) + n_tokens = int(atom_to_token.max().item()) + 1 + return c_base, attention_params, mask_exp, n_tokens + def forward( self, ref_pos: Tensor, @@ -579,43 +545,31 @@ def forward( ref_element: Tensor, ref_atom_name_chars: Tensor, atom_to_token: Tensor, - r_l: Tensor | None = None, - pred_r1: Tensor | None = None, + atom_coords: Tensor | None = None, + pred_coords: Tensor | None = None, num_diffusion_samples: int = 1, inference_cache: dict | None = None, ) -> tuple[Tensor, Tensor, Tensor, tuple]: - """Returns (a, q, c, attention_params). + """Returns (token_acts, atom_queries, atom_cond, attention_params). ``inference_cache`` caches step-invariant tensors (c_base, 3D RoPE, attention indices, n_tokens) across diffusion steps. """ - B, N = ref_pos.shape[:2] - layer_cache = None if inference_cache is not None: layer_cache = inference_cache.setdefault("atomencoder", {}) if layer_cache is None or len(layer_cache) == 0: - atom_feats = torch.cat( - [ - ref_pos, - ref_charge.unsqueeze(-1), - atom_attention_mask.unsqueeze(-1), - ref_element, - ref_atom_name_chars.reshape(B, N, self.char_feature_dim), - ], - dim=-1, - ) - c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( - self.atom_linear.weight.dtype + c_base, attention_params, mask_exp, n_tokens = self._compute_step_invariants( + ref_pos=ref_pos, + atom_attention_mask=atom_attention_mask, + ref_space_uid=ref_space_uid, + ref_charge=ref_charge, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_to_token=atom_to_token, + num_diffusion_samples=num_diffusion_samples, ) - cos, sin = self.atom_transformer._build_3d_rope(ref_pos, ref_space_uid) - cos = cos.repeat_interleave(num_diffusion_samples, 0) - sin = sin.repeat_interleave(num_diffusion_samples, 0) - mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) - indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() - attention_params = (cos, sin, indices) - n_tokens = int(atom_to_token.max().item()) + 1 if layer_cache is not None: layer_cache["c_base"] = c_base layer_cache["attention_params"] = attention_params @@ -628,39 +582,34 @@ def forward( mask_exp = layer_cache["mask_exp"] n_tokens = layer_cache["n_tokens"] - c = c_base + atom_cond = c_base - q = c + atom_queries = atom_cond - if self.structure_prediction and r_l is not None: - q = q.repeat_interleave(num_diffusion_samples, 0) - if pred_r1 is None: - pred_r1 = torch.zeros_like(r_l) - r_input = torch.cat([r_l, pred_r1], dim=-1) - r_to_q = self.coords_linear(r_input.to(self.coords_linear.weight.dtype)) - q = q + r_to_q + if self.structure_prediction and atom_coords is not None: + atom_queries = atom_queries.repeat_interleave(num_diffusion_samples, 0) + if pred_coords is None: + pred_coords = torch.zeros_like(atom_coords) + coord_input = torch.cat([atom_coords, pred_coords], dim=-1) + coords_to_queries = self.coords_linear(coord_input.to(self.coords_linear.weight.dtype)) + atom_queries = atom_queries + coords_to_queries - c = c.repeat_interleave(num_diffusion_samples, 0) + atom_cond = atom_cond.repeat_interleave(num_diffusion_samples, 0) - q = self.atom_transformer( - q_l=q, - c_l=c, + atom_queries = self.atom_transformer( + atom_queries=atom_queries, + atom_cond=atom_cond, attention_params=attention_params, ) - q_to_a = F.relu(self.atom_to_token_linear(q)) + queries_to_acts = F.relu(self.atom_to_token_linear(atom_queries)) if layer_cache is not None and "atom_to_token_exp" in layer_cache: atom_to_token_exp = layer_cache["atom_to_token_exp"] else: atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - a = scatter_atom_to_token(q_to_a, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) + token_acts = scatter_atom_to_token(queries_to_acts, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) - return a, q, c, attention_params - - -# =========================================================================== -# Atom-token utilities -# =========================================================================== + return token_acts, atom_queries, atom_cond, attention_params def _gather_along_dim1(source: Tensor, index: Tensor) -> Tensor: @@ -682,11 +631,6 @@ def gather_token_to_atom(token_features: Tensor, atom_to_token_idx: Tensor) -> T return _gather_along_dim1(token_features, atom_to_token_idx) -# =========================================================================== -# ESMFold2AtomDecoder -# =========================================================================== - - class ESMFold2AtomDecoder(nn.Module): """SWA atom decoder with token_to_atom_linear, atom_transformer, norm, output_linear. @@ -706,33 +650,28 @@ def __init__(self, config: ESMFold2Config) -> None: def forward( self, - a_i: Tensor, - q_l: Tensor, - c_l: Tensor, - p_lm: tuple, + token_acts: Tensor, + atom_queries: Tensor, + atom_cond: Tensor, + atom_pair: tuple, atom_to_token: Tensor, atom_attention_mask: Tensor, num_diffusion_samples: int = 1, ) -> Tensor: - """Returns r_update.""" + """Returns coord_update.""" atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) - a_to_q = self.token_to_atom_linear(a_i) + a_to_q = self.token_to_atom_linear(token_acts) a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) - q_l = q_l + a_to_q + atom_queries = atom_queries + a_to_q - q_l = self.atom_transformer( - q_l=q_l, - c_l=c_l, - attention_params=p_lm, + atom_queries = self.atom_transformer( + atom_queries=atom_queries, + atom_cond=atom_cond, + attention_params=atom_pair, ) - r_l = self.output_linear(self.norm(q_l)) - return r_l - - -# =========================================================================== -# ESMFold2AttentionPairBias (ESMFold2DiffusionTransformer attention block) -# =========================================================================== + atom_coords = self.output_linear(self.norm(atom_queries)) + return atom_coords class ESMFold2AttentionPairBias(nn.Module): @@ -765,32 +704,32 @@ def __init__( self.pair_norm = ESMFold2LayerNorm(d_pair, eps=1e-5) self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) - def compute_pair_bias(self, z: Tensor, bsz: int, num_diffusion_samples: int = 1) -> Tensor: + def compute_pair_bias(self, pair_repr: Tensor, bsz: int, num_diffusion_samples: int = 1) -> Tensor: """Project the (normed) pair representation to per-head attention biases. - Depends only on ``z`` and this block's fixed weights, so it is invariant + Depends only on ``pair_repr`` and this block's fixed weights, so it is invariant across diffusion sampling steps — the sampler computes it once and reuses it (see ``ESMFold2DiffusionTransformer.forward``). Bit-identical to computing it inline every step. """ - if z.dim() == 4 and z.shape[0] != bsz and num_diffusion_samples > 1: - z = z.repeat_interleave(num_diffusion_samples, dim=0) - if z.dim() == 4: - return self.pair_bias_proj(self.pair_norm(z)) - return z.unsqueeze(-1) + if pair_repr.dim() == 4 and pair_repr.shape[0] != bsz and num_diffusion_samples > 1: + pair_repr = pair_repr.repeat_interleave(num_diffusion_samples, dim=0) + if pair_repr.dim() == 4: + return self.pair_bias_proj(self.pair_norm(pair_repr)) + return pair_repr.unsqueeze(-1) def forward( self, - a: Tensor, - s: Tensor, - z: Tensor, + token_acts: Tensor, + single_repr: Tensor, + pair_repr: Tensor, attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, pair_bias: Tensor | None = None, ) -> Tensor: - bsz, n_queries, d_model = a.shape + bsz, n_queries, d_model = token_acts.shape - x = self.adaln(a, s) + x = self.adaln(token_acts, single_repr) n_keys = x.shape[1] q = self.q_proj(x).view(bsz, n_queries, self.num_heads, self.head_dim) @@ -802,12 +741,12 @@ def forward( if attention_mask is not None and attention_mask.shape[0] != bsz and num_diffusion_samples > 1: attention_mask = attention_mask.repeat_interleave(num_diffusion_samples, dim=0) - g = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) + gate = torch.sigmoid(self.g_proj(x)).view(bsz, n_queries, self.num_heads, self.head_dim) # ``pair_bias`` is step-invariant; the diffusion sampler precomputes and # caches it across steps. Compute inline when not supplied (e.g. uncached). if pair_bias is None: - pair_bias = self.compute_pair_bias(z, bsz, num_diffusion_samples) + pair_bias = self.compute_pair_bias(pair_repr, bsz, num_diffusion_samples) attn_bias = pair_bias.permute(0, 3, 1, 2) # [B,Q,K,H]->[B,H,Q,K] (H may be 1) if attention_mask is not None: @@ -815,20 +754,15 @@ def forward( mask_bias = torch.where(attention_mask.bool()[:, None, None, :], 0.0, min_val) attn_bias = attn_bias + mask_bias qh, kh, vh = (t.transpose(1, 2) for t in (q, k, v)) # [B,H,Q,D] - ctx = F.scaled_dot_product_attention(qh, kh, vh, attn_mask=attn_bias.to(qh.dtype), scale=self.scale) - ctx = ctx.transpose(1, 2) # [B,H,Q,D]->[B,Q,H,D] + context = F.scaled_dot_product_attention(qh, kh, vh, attn_mask=attn_bias.to(qh.dtype), scale=self.scale) + context = context.transpose(1, 2) # [B,H,Q,D]->[B,Q,H,D] - ctx = g * ctx - out = self.out_proj(ctx.reshape(bsz, n_queries, d_model).to(v.dtype)) - out = torch.sigmoid(self.out_gate(s)) * out + context = gate * context + out = self.out_proj(context.reshape(bsz, n_queries, d_model).to(v.dtype)) + out = torch.sigmoid(self.out_gate(single_repr)) * out return out -# =========================================================================== -# ESMFold2ConditionedTransitionBlock -# =========================================================================== - - class ESMFold2ConditionedTransitionBlock(nn.Module): """Conditioned ESMFold2SwiGLU transition with adaptive layer norm.""" @@ -846,23 +780,12 @@ def __init__( # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. self.output_gate = nn.Linear(d_cond, d_model, bias=True) - self.lin_swish = nn.Linear(d_model, 2 * hidden, bias=False) - self.lin_out = nn.Linear(hidden, d_model, bias=False) - - def forward(self, a: Tensor, s: Tensor) -> Tensor: - x = self.adaln(a, s) - - swish_a, swish_b = self.lin_swish(x).chunk(2, dim=-1) - b = F.silu(swish_a) * swish_b - out = self.lin_out(b) - - out = torch.sigmoid(self.output_gate(s)) * out - return out + self.ffn = ESMFold2SwiGLU(d_model, hidden, d_model, bias=False) - -# =========================================================================== -# ESMFold2DiffusionTransformer (token transformer) -# =========================================================================== + def forward(self, token_acts: Tensor, single_repr: Tensor) -> Tensor: + x = self.adaln(token_acts, single_repr) + out = self.ffn(x) + return torch.sigmoid(self.output_gate(single_repr)) * out class ESMFold2DiffusionTransformer(nn.Module): @@ -871,11 +794,11 @@ class ESMFold2DiffusionTransformer(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() dm = config.structure_head.diffusion_module - d_model = dm.c_token - d_pair = dm.c_z + d_model = dm.token_hidden_size + d_pair = config.pairwise_hidden_size # == the (removed) dm.c_z; the trunk pair rep flows in at this width num_heads = dm.token_num_heads num_blocks = dm.token_num_blocks - d_cond = dm.c_token + d_cond = dm.token_hidden_size transition_multiplier = dm.transition_multiplier self.attn_blocks = nn.ModuleList( @@ -902,54 +825,52 @@ def __init__(self, config: ESMFold2Config) -> None: def forward( self, - a: Tensor, - s: Tensor, - z: Tensor, + token_acts: Tensor, + single_repr: Tensor, + pair_repr: Tensor, attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, inference_cache: dict | None = None, ) -> Tensor: - x = a - bsz = a.shape[0] + x = token_acts + bsz = token_acts.shape[0] # Each block's pair bias depends only on the (step-invariant) conditioning - # pair ``z`` and fixed weights, so compute it once per block and reuse it + # pair repr and fixed weights, so compute it once per block and reuse it # across every diffusion sampling step. Bit-identical to recomputing it # each step; the cache lives in the sampler's per-fold ``inference_cache``. bias_cache = None if inference_cache is None else inference_cache.setdefault("token_pair_bias", {}) for i, (attn, transition) in enumerate(zip(self.attn_blocks, self.transition_blocks)): if bias_cache is None: - pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) + pair_bias = attn.compute_pair_bias(pair_repr, bsz, num_diffusion_samples) elif i in bias_cache: pair_bias = bias_cache[i] else: - pair_bias = attn.compute_pair_bias(z, bsz, num_diffusion_samples) + pair_bias = attn.compute_pair_bias(pair_repr, bsz, num_diffusion_samples) bias_cache[i] = pair_bias x = x + attn( x, - s, - z, + single_repr, + pair_repr, attention_mask=attention_mask, num_diffusion_samples=num_diffusion_samples, pair_bias=pair_bias, ) - x = x + transition(x, s) + x = x + transition(x, single_repr) return x -# =========================================================================== -# ESMFold2DiffusionConditioning -# =========================================================================== - - class ESMFold2DiffusionConditioning(nn.Module): """Conditions pair and single representations on noise timestep.""" def __init__(self, config: ESMFold2Config) -> None: super().__init__() dm = config.structure_head.diffusion_module - c_z = dm.c_z - c_s = dm.c_token # the conditioning's single output is sized to c_token - c_s_inputs = dm.c_s_inputs + # c_z / c_s_inputs are forced equal to the parent's d_pair / inputs.d_inputs (the trunk pair + # rep and the embedder single-inputs tensor flow straight into the norms below), so read the + # canonical fields rather than re-declaring them on DiffusionModuleConfig. + c_z = config.pairwise_hidden_size + c_s = dm.token_hidden_size # the conditioning's single output is sized to c_token + c_s_inputs = config.inputs.single_inputs_size fourier_dim = dm.fourier_dim transition_multiplier = dm.transition_multiplier # The norms/transitions use their default eps (1e-5), as before. @@ -972,58 +893,55 @@ def __init__(self, config: ESMFold2Config) -> None: def forward( self, t_hat: Tensor, - s_inputs: Tensor, - z_trunk: Tensor, + single_inputs: Tensor, + pair_trunk: Tensor, relative_position_encoding: Tensor, sigma_data: float | None = None, num_diffusion_samples: int = 1, inference_cache: dict[str, Tensor] | None = None, ) -> tuple[Tensor, Tensor]: sigma = self.sigma_data if sigma_data is None else float(sigma_data) - base_batch = z_trunk.shape[0] + base_batch = pair_trunk.shape[0] target_batch = base_batch * num_diffusion_samples - # z conditioning (cached across diffusion steps — independent of t_hat) + # pair conditioning (cached across diffusion steps — independent of t_hat) if inference_cache is not None and "z" in inference_cache: - z = inference_cache["z"] + pair_repr = inference_cache["z"] else: - z_rel = relative_position_encoding.to(dtype=torch.float32) - z = torch.cat([z_trunk.to(dtype=torch.float32), z_rel], dim=-1) + rel_pos = relative_position_encoding.to(dtype=torch.float32) + pair_repr = torch.cat([pair_trunk.to(dtype=torch.float32), rel_pos], dim=-1) # The relpos/coords conditioning is fp32; z_input_norm keeps it fp32, # then we hand off to z_proj in the model's compute dtype. - z = self.z_proj(self.z_input_norm(z).to(self.z_proj.weight.dtype)) + pair_repr = self.z_proj(self.z_input_norm(pair_repr).to(self.z_proj.weight.dtype)) for block in self.z_transitions: - z = z + block(z) + pair_repr = pair_repr + block(pair_repr) if inference_cache is not None: - inference_cache["z"] = z + inference_cache["z"] = pair_repr - # s conditioning - s_inputs_eff = s_inputs - if s_inputs_eff.shape[0] != target_batch: - s_inputs_eff = s_inputs_eff.repeat_interleave(num_diffusion_samples, 0) + # single conditioning + single_inputs_eff = single_inputs + if single_inputs_eff.shape[0] != target_batch: + single_inputs_eff = single_inputs_eff.repeat_interleave(num_diffusion_samples, 0) - s = self.s_proj(self.s_input_norm(s_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype)) + single_repr = self.s_proj( + self.s_input_norm(single_inputs_eff.to(dtype=torch.float32)).to(self.s_proj.weight.dtype) + ) # Noise embedding - t = torch.as_tensor(t_hat, dtype=torch.float32, device=s.device).reshape(-1) + t = torch.as_tensor(t_hat, dtype=torch.float32, device=single_repr.device).reshape(-1) if t.numel() == 1: t = t.expand(target_batch) elif t.shape[0] != target_batch: t = t.repeat_interleave(num_diffusion_samples, 0) t_noise = 0.25 * torch.log((t / sigma).clamp(min=1e-20)) - n = self.fourier(t_noise) - n = self.noise_proj(self.noise_norm(n.float()).to(self.noise_proj.weight.dtype)) - s = s + n.unsqueeze(1) + noise_emb = self.fourier(t_noise) + noise_emb = self.noise_proj(self.noise_norm(noise_emb.float()).to(self.noise_proj.weight.dtype)) + single_repr = single_repr + noise_emb.unsqueeze(1) for block in self.s_transitions: - s = s + block(s) - - return s, z - + single_repr = single_repr + block(single_repr) -# =========================================================================== -# ESMFold2DiffusionModule -# =========================================================================== + return single_repr, pair_repr class ESMFold2DiffusionModule(nn.Module): @@ -1032,7 +950,7 @@ class ESMFold2DiffusionModule(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() dm = config.structure_head.diffusion_module - c_token = dm.c_token + c_token = dm.token_hidden_size self.sigma_data = float(dm.sigma_data) self.conditioning = ESMFold2DiffusionConditioning(config) @@ -1063,8 +981,8 @@ def forward( ref_atom_name_chars: Tensor, ref_space_uid: Tensor, tok_idx: Tensor, - s_inputs: Tensor, - z_trunk: Tensor, + single_inputs: Tensor, + pair_trunk: Tensor, relative_position_encoding: Tensor, asym_id: Tensor, residue_index: Tensor, @@ -1082,11 +1000,11 @@ def forward( if t.numel() == 1: t = t.expand(bsz) - # Step 1: conditioning (pair z is cached across diffusion steps) - s, z = self.conditioning( + # Step 1: conditioning (pair repr is cached across diffusion steps) + single_repr, pair_repr = self.conditioning( t_hat=t, - s_inputs=s_inputs, - z_trunk=z_trunk, + single_inputs=single_inputs, + pair_trunk=pair_trunk, relative_position_encoding=relative_position_encoding, sigma_data=sigma, num_diffusion_samples=num_diffusion_samples, @@ -1095,10 +1013,10 @@ def forward( # Step 2: normalize noisy coords denom = torch.sqrt(t * t + sigma * sigma) - r_noisy = x_noisy / denom[:, None, None] + normalized_coords = x_noisy / denom[:, None, None] # Step 3: atom encoder - a, q_skip, c_skip, p_skip = self.atom_encoder( + token_acts, atom_queries_skip, atom_cond_skip, atom_pair_skip = self.atom_encoder( ref_pos=ref_pos, atom_attention_mask=ref_mask, ref_space_uid=ref_space_uid, @@ -1106,33 +1024,33 @@ def forward( ref_element=ref_element, ref_atom_name_chars=ref_atom_name_chars, atom_to_token=tok_idx, - r_l=r_noisy, + atom_coords=normalized_coords, num_diffusion_samples=num_diffusion_samples, inference_cache=inference_cache, ) - # Step 4: add conditioned s - a = a + self.s_to_token(self.s_step_norm(s)) + # Step 4: add conditioned single repr + token_acts = token_acts + self.s_to_token(self.s_step_norm(single_repr)) # Step 5: token transformer (pair bias is cached across steps via inference_cache) - a = self.token_transformer( - a, - s, - z, + token_acts = self.token_transformer( + token_acts, + single_repr, + pair_repr, attention_mask=token_attention_mask, num_diffusion_samples=num_diffusion_samples, inference_cache=inference_cache, ) # Step 6: token norm - a = self.token_norm(a) + token_acts = self.token_norm(token_acts) # Step 7: atom decoder - r_update = self.atom_decoder( - a_i=a, - q_l=q_skip, - c_l=c_skip, - p_lm=p_skip, + coord_update = self.atom_decoder( + token_acts=token_acts, + atom_queries=atom_queries_skip, + atom_cond=atom_cond_skip, + atom_pair=atom_pair_skip, atom_to_token=tok_idx, atom_attention_mask=ref_mask, num_diffusion_samples=num_diffusion_samples, @@ -1142,16 +1060,11 @@ def forward( sigma2 = sigma * sigma t2 = t * t out = (sigma2 / (sigma2 + t2))[:, None, None] * x_noisy - out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * r_update + out = out + ((sigma * t) / torch.sqrt(sigma2 + t2))[:, None, None] * coord_update return out -# =========================================================================== -# ESMFold2DiffusionStructureHead -# =========================================================================== - - class ESMFold2DiffusionStructureHead(nn.Module): """Wrapper around ESMFold2DiffusionModule with diffusion sampling.""" @@ -1187,8 +1100,8 @@ def inference_noise_schedule(self, num_steps: int | None = None, device: torch.d ) p = float(self.inference_p) inv_p = 1.0 / p - k = torch.arange(steps, device=device, dtype=torch.float32) - base = self.inference_s_max**inv_p + (k / (steps - 1)) * ( + ramp = torch.arange(steps, device=device, dtype=torch.float32) + base = self.inference_s_max**inv_p + (ramp / (steps - 1)) * ( self.inference_s_min**inv_p - self.inference_s_max**inv_p ) schedule = self.sigma_data * base.pow(p) @@ -1262,10 +1175,30 @@ def _weighted_rigid_align(x: Tensor, x_gt: Tensor, w: Tensor, mask: Tensor) -> T # ------------------------------------------------------------------ @torch.inference_mode() + def _build_noise_schedule( + self, num_sampling_steps: int | None, device: torch.device, max_inference_sigma: float | None + ) -> tuple[Tensor, Tensor]: + """Karras σ schedule (Algorithm 18) + per-step γ churn factors. + + When ``max_inference_sigma`` caps the schedule, the high-σ tail is truncated and the cap + is re-prepended so sampling still starts from the requested σ (see ``sample`` docstring). + """ + steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) + schedule = self.inference_noise_schedule(steps, device) + if max_inference_sigma is not None: + schedule = schedule[schedule <= float(max_inference_sigma)] + schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) + gammas = torch.where( + schedule > self.gamma_min, + torch.full_like(schedule, self.gamma_0), + torch.zeros_like(schedule), + ) + return schedule, gammas + def sample( self, - z_trunk: Tensor, - s_inputs: Tensor, + pair_trunk: Tensor, + single_inputs: Tensor, relative_position_encoding: Tensor, ref_pos: Tensor, ref_charge: Tensor, @@ -1296,17 +1229,12 @@ def sample( requested step count post-truncation. """ n_atoms = tok_idx.shape[1] - device = s_inputs.device - target_batch = s_inputs.shape[0] * num_diffusion_samples + device = single_inputs.device + target_batch = single_inputs.shape[0] * num_diffusion_samples inference_cache: dict[str, Tensor] | None = {} if use_inference_cache else None - steps = self.inference_num_steps if num_sampling_steps is None else int(num_sampling_steps) - - schedule = self.inference_noise_schedule(steps, device) - if max_inference_sigma is not None: - schedule = schedule[schedule <= float(max_inference_sigma)] - schedule = F.pad(schedule, (1, 0), value=float(max_inference_sigma)) + schedule, gammas = self._build_noise_schedule(num_sampling_steps, device, max_inference_sigma) lam = self.noise_scale if noise_scale is None else float(noise_scale) eta = self.step_scale if step_scale is None else float(step_scale) @@ -1314,12 +1242,6 @@ def sample( x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() - gammas = torch.where( - schedule > self.gamma_min, - torch.full_like(schedule, self.gamma_0), - torch.zeros_like(schedule), - ) - x_denoised_prev: Tensor | None = None step_pairs = list(zip(schedule[:-1], schedule[1:], gammas[1:])) @@ -1342,8 +1264,8 @@ def sample( ref_atom_name_chars=ref_atom_name_chars, ref_space_uid=ref_space_uid, tok_idx=tok_idx, - s_inputs=s_inputs, - z_trunk=z_trunk, + single_inputs=single_inputs, + pair_trunk=pair_trunk, relative_position_encoding=relative_position_encoding, asym_id=asym_id, residue_index=residue_index, @@ -1369,11 +1291,6 @@ def sample( return {"sample_atom_coords": x} -# =========================================================================== -# ESMFold2RowAttentionPooling -# =========================================================================== - - class ESMFold2RowAttentionPooling(nn.Module): """Row-wise attention pooling: attn_proj, out_proj.""" @@ -1395,11 +1312,6 @@ def forward(self, z: Tensor, mask: Tensor) -> Tensor: return self.out_proj(pooled) -# =========================================================================== -# ESMFold2InputsEmbedder -# =========================================================================== - - class ESMFold2InputsEmbedder(nn.Module): """Embeds input features including atom-level encoding via SWA attention.""" @@ -1445,9 +1357,16 @@ def forward( ) -# =========================================================================== -# ESMFold2ResIdxAsymIdSymIdEntityIdEncoding (trunk relative position) -# =========================================================================== +def _relative_position_one_hot(diff: Tensor, n_bins: int, keep_mask: Tensor) -> Tensor: + """One-hot encode a relative index difference into ``2 * n_bins + 2`` classes. + + Classes ``[0, 2 * n_bins]`` hold the clipped relative offset; the final class + ``2 * n_bins + 1`` is the "out-of-context" bin assigned wherever ``keep_mask`` is False + (e.g. a pair spanning two chains). + """ + binned = torch.clip(diff + n_bins, 0, 2 * n_bins) + binned = torch.where(keep_mask, binned, 2 * n_bins + 1) + return F.one_hot(binned, 2 * n_bins + 2) class ESMFold2ResIdxAsymIdSymIdEntityIdEncoding(nn.Module): @@ -1486,34 +1405,19 @@ def forward( bij_same_residue = residue_index.unsqueeze(2) == residue_index.unsqueeze(1) bij_same_entity = entity_id.unsqueeze(2) == entity_id.unsqueeze(1) - dij_residue = residue_index.unsqueeze(2) - residue_index.unsqueeze(1) - dij_residue = torch.clip( - dij_residue + self.n_relative_residx_bins, - 0, - 2 * self.n_relative_residx_bins, + # Three relative encodings, all clip -> mask-out -> one-hot (see _relative_position_one_hot): + # residue offset within a chain, token offset within a residue, and chain (sym_id) offset + # across chains. The chain encoding keeps *cross*-chain pairs, so its mask is inverted. + residx_bins, chain_bins = self.n_relative_residx_bins, self.n_relative_chain_bins + aij_rel_pos = _relative_position_one_hot( + residue_index.unsqueeze(2) - residue_index.unsqueeze(1), residx_bins, bij_same_chain ) - dij_residue = torch.where(bij_same_chain, dij_residue, 2 * self.n_relative_residx_bins + 1) - aij_rel_pos = F.one_hot(dij_residue, 2 * self.n_relative_residx_bins + 2) - - dij_token = torch.clip( - token_index.unsqueeze(2) - token_index.unsqueeze(1) + self.n_relative_residx_bins, - 0, - 2 * self.n_relative_residx_bins, + aij_rel_token = _relative_position_one_hot( + token_index.unsqueeze(2) - token_index.unsqueeze(1), residx_bins, bij_same_chain & bij_same_residue ) - dij_token = torch.where( - bij_same_chain & bij_same_residue, - dij_token, - 2 * self.n_relative_residx_bins + 1, + aij_rel_chain = _relative_position_one_hot( + sym_id.unsqueeze(2) - sym_id.unsqueeze(1), chain_bins, ~bij_same_chain ) - aij_rel_token = F.one_hot(dij_token, 2 * self.n_relative_residx_bins + 2) - - dij_chain = torch.clip( - sym_id.unsqueeze(2) - sym_id.unsqueeze(1) + self.n_relative_chain_bins, - 0, - 2 * self.n_relative_chain_bins, - ) - dij_chain = torch.where(bij_same_chain, 2 * self.n_relative_chain_bins + 1, dij_chain) - aij_rel_chain = F.one_hot(dij_chain, 2 * self.n_relative_chain_bins + 2) feats = torch.cat( [ @@ -1528,22 +1432,14 @@ def forward( return self.embed(feats.to(self.embed.weight.dtype)) -# =========================================================================== -# ESMFold2SingleToPair (for ESMFold2LanguageModelShim) -# =========================================================================== - - class ESMFold2SingleToPair(nn.Module): - """downproject, output_mlp (Sequential of Linear, GELU, Linear).""" + """downproject -> outer product/difference -> two-layer MLP (fc1, GELU, fc2).""" def __init__(self, input_dim: int, downproject_dim: int, output_dim: int) -> None: super().__init__() self.downproject = nn.Linear(input_dim, downproject_dim) - self.output_mlp = nn.Sequential( - nn.Linear(2 * downproject_dim, output_dim), - nn.GELU(), - nn.Linear(output_dim, output_dim), - ) + self.output_fc1 = nn.Linear(2 * downproject_dim, output_dim) + self.output_fc2 = nn.Linear(output_dim, output_dim) def forward(self, x: Tensor) -> Tensor: x = self.downproject(x) @@ -1551,12 +1447,7 @@ def forward(self, x: Tensor) -> Tensor: [(x.unsqueeze(2) * x.unsqueeze(1)), (x.unsqueeze(2) - x.unsqueeze(1))], dim=3, ) - return self.output_mlp(x) - - -# =========================================================================== -# ESMFold2LanguageModelShim -# =========================================================================== + return self.output_fc2(F.gelu(self.output_fc1(x))) class ESMFold2LanguageModelShim(nn.Module): @@ -1564,15 +1455,17 @@ class ESMFold2LanguageModelShim(nn.Module): Contains: - base_z_combine: nn.Parameter [num_layers+1] - - base_z_linear: Sequential(ESMFold2LayerNorm(d_model), Linear(d_model, d_z, bias=False)) - - base_z_mlp: Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) + - base_z_input_norm -> base_z_proj: ESMFold2LayerNorm(d_model) then Linear(d_model, d_z, bias=False) + - base_z_to_pair -> base_z_output_norm: ESMFold2SingleToPair(d_z, d_z, d_z) then ESMFold2LayerNorm(d_z) """ def __init__(self, d_z: int = 256, d_model: int = 2560, num_layers: int = 80) -> None: super().__init__() - self.base_z_mlp = nn.Sequential(ESMFold2SingleToPair(d_z, d_z, d_z), ESMFold2LayerNorm(d_z)) - self.base_z_linear = nn.Sequential(ESMFold2LayerNorm(d_model), nn.Linear(d_model, d_z, bias=False)) + self.base_z_to_pair = ESMFold2SingleToPair(d_z, d_z, d_z) + self.base_z_output_norm = ESMFold2LayerNorm(d_z) + self.base_z_input_norm = ESMFold2LayerNorm(d_model) + self.base_z_proj = nn.Linear(d_model, d_z, bias=False) self.base_z_combine = nn.Parameter(torch.zeros(num_layers + 1)) def forward(self, hidden_states: Tensor) -> Tensor: @@ -1586,15 +1479,15 @@ def forward(self, hidden_states: Tensor) -> Tensor: """ # The ESMC backbone may be loaded at a different precision than the trunk # (e.g. bf16 backbone with an fp32 trunk); align to the projection dtype. - hidden_states = hidden_states.to(self.base_z_linear[1].weight.dtype) - # base_z_linear[0] is an fp32-pinned LayerNorm; upcast in, downcast out. - normed = self.base_z_linear[0](hidden_states) - lm_z = self.base_z_linear[1](normed) # [B, L, 81, d_z] + hidden_states = hidden_states.to(self.base_z_proj.weight.dtype) + # base_z_input_norm is an fp32-pinned LayerNorm; upcast in, downcast out. + normed = self.base_z_input_norm(hidden_states) + lm_z = self.base_z_proj(normed) # [B, L, 81, d_z] weights = self.base_z_combine.softmax(0) # [81] lm_z = (weights @ lm_z).squeeze(-2) # [B, L, d_z] - # base_z_mlp[1] is an fp32-pinned LayerNorm; upcast in, downcast out. - pair = self.base_z_mlp[0](lm_z) - lm_z = self.base_z_mlp[1](pair) # [B, L, L, d_z] + # base_z_output_norm is an fp32-pinned LayerNorm; upcast in, downcast out. + pair = self.base_z_to_pair(lm_z) + lm_z = self.base_z_output_norm(pair) # [B, L, L, d_z] return lm_z @@ -1603,9 +1496,6 @@ def forward(self, hidden_states: Tensor) -> Tensor: _DEFAULT_CHUNK_SIZE = 64 -# =========================================================================== -# ESMFold2TriangleMultiplicativeUpdate -# =========================================================================== @use_kernel_forward_from_hub("ESMFold2TriangleMultiplication") class ESMFold2TriangleMultiplicativeBlock(nn.Module): """Triangle multiplicative update block with gated signal routing. @@ -1692,11 +1582,6 @@ def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: return self._engine(z, visibility=mask) -# =========================================================================== -# ESMFold2FoldingTrunk: ESMFold2Transition, ESMFold2PairUpdateBlock, ESMFold2FoldingTrunk -# =========================================================================== - - class ESMFold2Transition(nn.Module): """LayerNorm + ESMFold2SwiGLU feed-forward residual block, chunked along the token axis.""" @@ -1714,10 +1599,10 @@ def forward(self, x: Tensor) -> Tensor: if self._chunk_size is None or x.shape[1] <= self._chunk_size: return x + self.ffn(self.norm(x)) out_list: list[Tensor] = [] - for s in range(0, x.shape[1], self._chunk_size): - e = min(s + self._chunk_size, x.shape[1]) - sl = x[:, s:e] - out_list.append(sl + self.ffn(self.norm(sl))) + for start in range(0, x.shape[1], self._chunk_size): + end = min(start + self._chunk_size, x.shape[1]) + chunk_x = x[:, start:end] + out_list.append(chunk_x + self.ffn(self.norm(chunk_x))) return torch.cat(out_list, dim=1) @@ -1758,19 +1643,14 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Tensor: for block in self.blocks: - fn = partial(block, pair_attention_mask=pair_attention_mask) + block_fn = partial(block, pair_attention_mask=pair_attention_mask) if torch.is_grad_enabled(): - pair = checkpoint(fn, pair, use_reentrant=False) + pair = checkpoint(block_fn, pair, use_reentrant=False) else: - pair = fn(pair) + pair = block_fn(pair) return pair -# =========================================================================== -# MSA Encoder -# =========================================================================== - - class ESMFold2OuterProductMean(nn.Module): """Outer-product mean: maps an MSA representation into a pair update. @@ -1803,28 +1683,28 @@ def __init__( def set_chunk_size(self, chunk_size: int | None) -> None: self._chunk_size = chunk_size - def forward(self, m: Tensor, msa_attention_mask: Tensor) -> Tensor: - m_norm = self.norm(m) - x = self.W(m_norm) * msa_attention_mask.unsqueeze(-1).to(m_norm.dtype) - a, b = x.chunk(2, dim=-1) - mask_f = msa_attention_mask.to(a.dtype) + def forward(self, msa_repr: Tensor, msa_attention_mask: Tensor) -> Tensor: + msa_normed = self.norm(msa_repr) + x = self.W(msa_normed) * msa_attention_mask.unsqueeze(-1).to(msa_normed.dtype) + left, right = x.chunk(2, dim=-1) + mask_f = msa_attention_mask.to(left.dtype) n_valid = (mask_f @ mask_f.transpose(-1, -2)).unsqueeze(-1).clamp(min=1.0) if self._chunk_size is None: - outer = torch.einsum("bimc,bjmd->bijcd", a, b).flatten(-2) + outer = torch.einsum("bimc,bjmd->bijcd", left, right).flatten(-2) if self.divide_outer_before_proj: return self.Wout(outer / n_valid) return self.Wout(outer) / n_valid # Chunk along the left (i) axis so the peak einsum intermediate is # [B, chunk, L, c, d] instead of [B, L, L, c, d]. - L = a.shape[1] + L = left.shape[1] out_chunks: list[Tensor] = [] - for s in range(0, L, self._chunk_size): - e = min(s + self._chunk_size, L) - outer_chunk = torch.einsum("bimc,bjmd->bijcd", a[:, s:e], b).flatten(-2) + for start in range(0, L, self._chunk_size): + end = min(start + self._chunk_size, L) + outer_chunk = torch.einsum("bimc,bjmd->bijcd", left[:, start:end], right).flatten(-2) if self.divide_outer_before_proj: - out_chunks.append(self.Wout(outer_chunk / n_valid[:, s:e])) + out_chunks.append(self.Wout(outer_chunk / n_valid[:, start:end])) else: - out_chunks.append(self.Wout(outer_chunk) / n_valid[:, s:e]) + out_chunks.append(self.Wout(outer_chunk) / n_valid[:, start:end]) return torch.cat(out_chunks, dim=1) @@ -1836,7 +1716,8 @@ def __init__(self, d_msa: int, d_pair: int, n_heads: int = 8, head_width: int = self.n_heads = n_heads self.head_width = head_width self.norm_single = ESMFold2LayerNorm(d_msa) - self.compute_bias = nn.Sequential(ESMFold2LayerNorm(d_pair), nn.Linear(d_pair, n_heads, bias=False)) + self.bias_norm = ESMFold2LayerNorm(d_pair) + self.bias_proj = nn.Linear(d_pair, n_heads, bias=False) self.Wv = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wgate = nn.Linear(d_msa, n_heads * head_width, bias=False) self.Wout = nn.Linear(n_heads * head_width, d_msa, bias=False) @@ -1854,7 +1735,7 @@ def forward(self, msa_repr: Tensor, pair_repr: Tensor, pair_attention_mask: Tens h, dh = self.n_heads, self.head_width msa_normed = self.norm_single(msa_repr) - bias = self.compute_bias[1](self.compute_bias[0](pair_repr)) # [B, L, L, n_heads] + bias = self.bias_proj(self.bias_norm(pair_repr)) # [B, L, L, n_heads] bias.masked_fill_(~pair_attention_mask.unsqueeze(-1).bool(), -1e5) attn = torch.softmax(bias, dim=-2, dtype=torch.float32).to(bias.dtype) # softmax over j @@ -1924,19 +1805,6 @@ class ESMFold2Output(ModelOutput): pair_chains_iptm: Tensor | None = None -def gather_rep_atom_coords(coords: Tensor, rep_atom_idx: Tensor) -> Tensor: - """Gather representative atom coordinates for each token. - - Args: - coords: [B, A, 3] - rep_atom_idx: [B, L] int64 - - Returns: - [B, L, 3] - """ - return _gather_along_dim1(coords, rep_atom_idx) - - def _compute_intra_token_idx(atom_to_token: Tensor) -> Tensor: """Compute local atom index within each token (vectorised). @@ -1987,9 +1855,9 @@ class ESMFold2ConfidenceHead(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() ch = config.confidence_head - d_single = config.d_single - d_pair = config.d_pair - d_inputs = config.inputs.d_inputs + d_single = config.hidden_size + d_pair = config.pairwise_hidden_size + d_inputs = config.inputs.single_inputs_size boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1) self.register_buffer("boundaries", boundaries) @@ -2040,22 +1908,25 @@ def _flatten_sample_axis(x: Tensor) -> Tensor: return x.reshape(b * mult, n, c) return x - def forward( + def _build_pair_and_single( self, - s_inputs: Tensor, + single_inputs: Tensor, z: Tensor, x_pred: Tensor, distogram_atom_idx: Tensor, token_attention_mask: Tensor, atom_to_token: Tensor, atom_attention_mask: Tensor, - asym_id: Tensor, - mol_type: Tensor, - num_diffusion_samples: int = 1, - relative_position_encoding: Tensor | None = None, - token_bonds_encoding: Tensor | None = None, - ) -> dict[str, Tensor]: - s_inputs_normed = self.s_inputs_norm(s_inputs) + num_diffusion_samples: int, + relative_position_encoding: Tensor | None, + token_bonds_encoding: Tensor | None, + ) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor, Tensor, int]: + """Build the per-sample pair + single representations shared by every confidence head. + + Returns ``(single, pair, mask, rep_distances, rep_idx_m, atom_to_token_m, atom_mask_m, Bm)`` + where the ``*_m`` tensors are repeated across the diffusion-sample batch axis. + """ + s_inputs_normed = self.s_inputs_norm(single_inputs) z_base = self.z_norm(z) if relative_position_encoding is not None: @@ -2076,7 +1947,7 @@ def forward( mask = self._repeat_batch(token_attention_mask, num_diffusion_samples) Bm = pair.shape[0] - rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m) + rep_coords = _gather_along_dim1(x_pred_flat, rep_idx_m) rep_distances = torch.cdist(rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist") distogram_bins = (rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() pair = pair + self.dist_bin_pairwise_embed(distogram_bins) @@ -2092,6 +1963,20 @@ def forward( pair = pair.to(self.pae_head.weight.dtype) single = self.row_attention_pooling(pair, mask) + return single, pair, mask, rep_distances, rep_idx_m, atom_to_token_m, atom_mask_m, Bm + + def _compute_atom_confidences( + self, + single: Tensor, + atom_to_token_m: Tensor, + atom_mask_m: Tensor, + rep_idx_m: Tensor, + rep_distances: Tensor, + expanded_type: Tensor, + expanded_asym: Tensor, + Bm: int, + ) -> dict[str, Tensor]: + """Per-atom confidence outputs off the single representation (pLDDT family + resolved).""" atom_mask_f = atom_mask_m.float() s_at_atoms = gather_token_to_atom(single, atom_to_token_m) s_at_atoms_ln = self.plddt_ln(s_at_atoms) @@ -2112,8 +1997,6 @@ def forward( complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (atom_mask_f.sum(dim=-1) + _CONFIDENCE_EPS) - expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) - expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) is_ligand = (expanded_type == 4).float() # 4 = non-polymer (ligand) molecule type inter_chain = (expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)).float() near_contact = (rep_distances < 8).float() @@ -2129,20 +2012,25 @@ def forward( plddt_ca = plddt_per_atom.gather(1, rep_idx_m) - # PAE - pae_logits = self.pae_head(self.pae_ln(pair)) - pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() - - # PDE - pde_logits = self.pde_head(self.pde_ln(pair)) - pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() - - # Resolved (per-atom binary). + # Resolved (per-atom binary): same per-atom single features, its own weight. s_at_atoms_res = self.resolved_ln(s_at_atoms) w_res = self.resolved_weight[intra_idx] resolved_logits = torch.einsum("...c,...cb->...b", s_at_atoms_res, w_res) - # pTM / ipTM from pae_logits. + return { + "plddt_logits": plddt_logits, + "plddt": plddt.detach(), + "plddt_per_atom": plddt_per_atom.detach(), + "plddt_ca": plddt_ca.detach(), + "complex_plddt": complex_plddt.detach(), + "complex_iplddt": complex_iplddt.detach(), + "resolved_logits": resolved_logits, + } + + def _compute_ptm_iptm( + self, pae_logits: Tensor, mask: Tensor, expanded_asym: Tensor, Bm: int + ) -> tuple[Tensor, Tensor, Tensor]: + """pTM / ipTM / per-chain-pair ipTM derived from the PAE logits.""" n_bins = pae_logits.shape[-1] bin_width = 32.0 / n_bins bin_centers = torch.arange(0.5 * bin_width, 32.0, bin_width, device=pae_logits.device) @@ -2174,21 +2062,66 @@ def forward( denom = pair_m.sum(dim=(-1, -2)) + _CONFIDENCE_EPS pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(dim=(-1, -2)) / denom + return ptm.detach(), iptm.detach(), pair_chains_iptm.detach() + + def forward( + self, + single_inputs: Tensor, + z: Tensor, + x_pred: Tensor, + distogram_atom_idx: Tensor, + token_attention_mask: Tensor, + atom_to_token: Tensor, + atom_attention_mask: Tensor, + asym_id: Tensor, + mol_type: Tensor, + num_diffusion_samples: int = 1, + relative_position_encoding: Tensor | None = None, + token_bonds_encoding: Tensor | None = None, + ) -> dict[str, Tensor]: + single, pair, mask, rep_distances, rep_idx_m, atom_to_token_m, atom_mask_m, Bm = self._build_pair_and_single( + single_inputs=single_inputs, + z=z, + x_pred=x_pred, + distogram_atom_idx=distogram_atom_idx, + token_attention_mask=token_attention_mask, + atom_to_token=atom_to_token, + atom_attention_mask=atom_attention_mask, + num_diffusion_samples=num_diffusion_samples, + relative_position_encoding=relative_position_encoding, + token_bonds_encoding=token_bonds_encoding, + ) + + expanded_type = self._repeat_batch(mol_type, num_diffusion_samples) + expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples) + atom_confidences = self._compute_atom_confidences( + single=single, + atom_to_token_m=atom_to_token_m, + atom_mask_m=atom_mask_m, + rep_idx_m=rep_idx_m, + rep_distances=rep_distances, + expanded_type=expanded_type, + expanded_asym=expanded_asym, + Bm=Bm, + ) + + pae_logits = self.pae_head(self.pae_ln(pair)) + pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach() + + pde_logits = self.pde_head(self.pde_ln(pair)) + pde = _categorical_mean(pde_logits, start=0.0, end=32.0).detach() + + ptm, iptm, pair_chains_iptm = self._compute_ptm_iptm(pae_logits, mask, expanded_asym, Bm) + return { - "plddt_logits": plddt_logits, - "plddt": plddt.detach(), - "plddt_per_atom": plddt_per_atom.detach(), - "plddt_ca": plddt_ca.detach(), - "complex_plddt": complex_plddt.detach(), - "complex_iplddt": complex_iplddt.detach(), + **atom_confidences, "pae_logits": pae_logits, "pae": pae, "pde_logits": pde_logits, "pde": pde, - "resolved_logits": resolved_logits, - "ptm": ptm.detach(), - "iptm": iptm.detach(), - "pair_chains_iptm": pair_chains_iptm.detach(), + "ptm": ptm, + "iptm": iptm, + "pair_chains_iptm": pair_chains_iptm, } @@ -2233,7 +2166,7 @@ def _init_weights(self, module): elif isinstance(module, ESMFold2AdaptiveLayerNorm): init.ones_(module.s_scale) elif isinstance(module, ESMFold2SWAAtomBlock): - init.zeros_(module.adaln_modulation[1].weight) + init.zeros_(module.adaln_linear.weight) elif isinstance(module, ESMFold2AttentionPairBias): if getattr(module, "out_gate", None) is not None: init.zeros_(module.out_gate.weight) @@ -2248,11 +2181,6 @@ def _init_weights(self, module): init.zeros_(module.base_z_combine) -# =========================================================================== -# ESMFold2 — language-model backbone helpers -# =========================================================================== - - def compute_lm_hidden_states( esmc: nn.Module, input_ids: Tensor, @@ -2388,8 +2316,8 @@ class ESMFold2Model(ESMFold2PreTrainedModel): def __init__(self, config: ESMFold2Config) -> None: super().__init__(config) - d_inputs = config.inputs.d_inputs - d_pair = config.d_pair + d_inputs = config.inputs.single_inputs_size + d_pair = config.pairwise_hidden_size self.inputs_embedder = ESMFold2InputsEmbedder(config) self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False) @@ -2424,7 +2352,9 @@ def __init__(self, config: ESMFold2Config) -> None: self.parcae_log_delta = nn.Parameter(torch.empty(d_pair, dtype=torch.float32)) self.parcae_b_cont = nn.Parameter(torch.empty(d_pair, d_pair)) self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) - self.parcae_coda = ESMFold2FoldingTrunk(n_layers=config.parcae.coda_n_layers, d_pair=d_pair, expansion_ratio=4) + self.parcae_coda = ESMFold2FoldingTrunk( + n_layers=config.parcae.num_coda_layers, d_pair=d_pair, expansion_ratio=4 + ) # Heads -------------------------------------------------------------- self.structure_head = ESMFold2DiffusionStructureHead(config) @@ -2469,10 +2399,12 @@ def _compute_lm_hidden_states( ) def _discretized_dynamics(self) -> tuple[Tensor, Tensor]: + # Discretized linear state-space dynamics for the parcae recurrence: ``state_decay`` is + # the per-channel state transition (Ā), ``input_matrix`` the discretized input projection (B̄). delta = F.softplus(self.parcae_log_delta) - a = torch.exp(-delta * torch.exp(self.parcae_log_a)) - b = delta[:, None] * self.parcae_b_cont - return a, b + state_decay = torch.exp(-delta * torch.exp(self.parcae_log_a)) + input_matrix = delta[:, None] * self.parcae_b_cont + return state_decay, input_matrix def _init_pair_state(self, ref: Tensor) -> Tensor: std = math.sqrt(2.0 / (5.0 * ref.shape[-1])) @@ -2480,6 +2412,93 @@ def _init_pair_state(self, ref: Tensor) -> Tensor: nn.init.trunc_normal_(state, mean=0.0, std=std, a=-3 * std, b=3 * std) return state.to(dtype=ref.dtype) + def _prepare_features( + self, + res_type: Tensor, + tok_mask: Tensor, + msa: Tensor | None, + msa_attention_mask: Tensor | None, + deletion_mean: Tensor | None, + ref_element: Tensor, + ref_atom_name_chars: Tensor, + atom_attention_mask: Tensor, + atom_to_token: Tensor, + ) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: + """One-hot / mask the raw structural inputs into embedder-ready features. + + Returns ``(res_type_oh, profile, deletion_mean, ref_element_oh, + ref_atom_name_chars_oh, atom_to_token)`` with ``atom_to_token`` zeroed at padding. + """ + if res_type.dim() == 2: + res_type_oh = F.one_hot(res_type.long(), num_classes=self.config.num_res_types).float() + res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() + else: + res_type_oh = res_type.float() + + if msa is not None: + msa_oh_profile = F.one_hot(msa.long(), num_classes=self.config.num_res_types).float() + if msa_attention_mask is not None: + mask_f = msa_attention_mask.float().unsqueeze(-1) + msa_oh_profile = msa_oh_profile * mask_f + valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) + profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) + else: + profile = msa_oh_profile.mean(dim=1) + else: + profile = res_type_oh + + if deletion_mean is None: + deletion_mean = torch.zeros(res_type.shape[0], res_type.shape[1], device=res_type.device) + + ref_element_oh = F.one_hot(ref_element.long(), num_classes=self.config.max_atomic_number).float() + ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=self.config.char_vocab_size).float() + # Bias-free downstream Linears require zeroed padding. + atm_mask_f = atom_attention_mask.float() + ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) + ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(-1).unsqueeze(-1) + atom_to_token = atom_to_token * atom_attention_mask.long() + + return res_type_oh, profile, deletion_mean, ref_element_oh, ref_atom_name_chars_oh, atom_to_token + + def _build_msa_kwargs( + self, + msa: Tensor | None, + msa_attention_mask: Tensor | None, + has_deletion: Tensor | None, + deletion_value: Tensor | None, + tok_mask: Tensor, + single_inputs: Tensor, + ) -> dict | None: + """Assemble the transposed/padded one-hot MSA tensors the MSA encoder consumes.""" + if self.msa_encoder is None or msa is None: + return None + B_msa, M, L_msa = msa.shape + msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=self.config.num_res_types).float() + msa_attn = ( + msa_attention_mask.permute(0, 2, 1).float() + if msa_attention_mask is not None + else tok_mask[:, :, None].expand(-1, -1, M).float() + ) + # Bias-free ESMFold2MSAEncoder.embed requires zeroed padding. + msa_oh = msa_oh * msa_attn.unsqueeze(-1) + has_deletion_t = ( + has_deletion.permute(0, 2, 1).float() + if has_deletion is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + deletion_value_t = ( + deletion_value.permute(0, 2, 1).float() + if deletion_value is not None + else torch.zeros(B_msa, L_msa, M, device=msa.device) + ) + return { + "single_inputs": single_inputs, + "msa_oh": msa_oh, + "has_deletion": has_deletion_t, + "deletion_value": deletion_value_t, + "msa_attention_mask": msa_attn, + } + def _run_one_loop( self, z: Tensor, @@ -2487,8 +2506,8 @@ def _run_one_loop( lm_z: Tensor | None, _msa_kwargs: dict | None, pair_mask: Tensor, - a: Tensor, - b_mat: Tensor, + state_decay: Tensor, + input_matrix: Tensor, total_steps: int, ) -> Tensor: # Helper method (not inline) so per-iter L²×c_z locals free on return (else @@ -2525,7 +2544,7 @@ def _run_one_loop( z_inject_pair = z_inject_pair + refined_lm_z.to(z_inject_pair.dtype) injected_pair = self.parcae_input_norm(z_inject_pair) - z = a * z + F.linear(injected_pair.to(z.dtype), b_mat) + z = state_decay * z + F.linear(injected_pair.to(z.dtype), input_matrix) z = self.folding_trunk(z, pair_attention_mask=pair_mask) return z @@ -2571,36 +2590,21 @@ def forward( ) total_steps = max(1, n_loops + 1) - if res_type.dim() == 2: - res_type_oh = F.one_hot(res_type.long(), num_classes=self.config.num_res_types).float() - res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float() - else: - res_type_oh = res_type.float() - - if msa is not None: - msa_oh_profile = F.one_hot(msa.long(), num_classes=self.config.num_res_types).float() - if msa_attention_mask is not None: - mask_f = msa_attention_mask.float().unsqueeze(-1) - msa_oh_profile = msa_oh_profile * mask_f - valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1) - profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1) - else: - profile = msa_oh_profile.mean(dim=1) - else: - profile = res_type_oh - - if deletion_mean is None: - deletion_mean = torch.zeros(res_type.shape[0], res_type.shape[1], device=res_type.device) - - ref_element_oh = F.one_hot(ref_element.long(), num_classes=self.config.max_atomic_number).float() - ref_atom_name_chars_oh = F.one_hot(ref_atom_name_chars.long(), num_classes=self.config.char_vocab_size).float() - # Bias-free downstream Linears require zeroed padding. - atm_mask_f = atm_mask.float() - ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1) - ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(-1).unsqueeze(-1) - atom_to_token = atom_to_token * atm_mask.long() + res_type_oh, profile, deletion_mean, ref_element_oh, ref_atom_name_chars_oh, atom_to_token = ( + self._prepare_features( + res_type=res_type, + tok_mask=tok_mask, + msa=msa, + msa_attention_mask=msa_attention_mask, + deletion_mean=deletion_mean, + ref_element=ref_element, + ref_atom_name_chars=ref_atom_name_chars, + atom_attention_mask=atm_mask, + atom_to_token=atom_to_token, + ) + ) - x_inputs = self.inputs_embedder( + single_inputs = self.inputs_embedder( aatype=res_type_oh, profile=profile.float(), deletion_mean=deletion_mean.float(), @@ -2613,7 +2617,7 @@ def forward( atom_to_token=atom_to_token, ) - z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(x_inputs).unsqueeze(1) + z_init = self.z_init_1(single_inputs).unsqueeze(2) + self.z_init_2(single_inputs).unsqueeze(1) relative_position_encoding = self.rel_pos( residue_index=residue_index, @@ -2636,38 +2640,18 @@ def forward( z = self._init_pair_state(z_init) - a, b = self._discretized_dynamics() - a = a.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) - b_mat = b.to(device=z.device, dtype=z.dtype) - - _msa_kwargs: dict | None = None - if self.msa_encoder is not None and msa is not None: - B_msa, M, L_msa = msa.shape - msa_oh = F.one_hot(msa.permute(0, 2, 1).long(), num_classes=self.config.num_res_types).float() - msa_attn = ( - msa_attention_mask.permute(0, 2, 1).float() - if msa_attention_mask is not None - else tok_mask[:, :, None].expand(-1, -1, M).float() - ) - # Bias-free ESMFold2MSAEncoder.embed requires zeroed padding. - msa_oh = msa_oh * msa_attn.unsqueeze(-1) - hd = ( - has_deletion.permute(0, 2, 1).float() - if has_deletion is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - dv = ( - deletion_value.permute(0, 2, 1).float() - if deletion_value is not None - else torch.zeros(B_msa, L_msa, M, device=msa.device) - ) - _msa_kwargs = { - "x_inputs": x_inputs, - "msa_oh": msa_oh, - "has_deletion": hd, - "deletion_value": dv, - "msa_attention_mask": msa_attn, - } + state_decay, input_matrix = self._discretized_dynamics() + state_decay = state_decay.view(1, 1, 1, -1).to(device=z.device, dtype=z.dtype) + input_matrix = input_matrix.to(device=z.device, dtype=z.dtype) + + _msa_kwargs = self._build_msa_kwargs( + msa=msa, + msa_attention_mask=msa_attention_mask, + has_deletion=has_deletion, + deletion_value=deletion_value, + tok_mask=tok_mask, + single_inputs=single_inputs, + ) z = self._run_one_loop( z=z, @@ -2675,11 +2659,11 @@ def forward( lm_z=lm_z, _msa_kwargs=_msa_kwargs, pair_mask=pair_mask, - a=a, - b_mat=b_mat, + state_decay=state_decay, + input_matrix=input_matrix, total_steps=total_steps, ) - del z_init, lm_z, _msa_kwargs, a, b_mat + del z_init, lm_z, _msa_kwargs, state_decay, input_matrix z = self.parcae_readout(z) z = self.parcae_coda(z, pair_attention_mask=pair_mask) @@ -2688,8 +2672,8 @@ def forward( distogram_logits = self.distogram_head((z + z.transpose(-2, -3)).to(self.distogram_head.weight.dtype)) structure_output = self.structure_head.sample( - z_trunk=z, - s_inputs=x_inputs, + pair_trunk=z, + single_inputs=single_inputs, relative_position_encoding=relative_position_encoding, ref_pos=ref_pos, ref_charge=ref_charge, @@ -2713,7 +2697,7 @@ def forward( raise RuntimeError("The diffusion structure head did not return sampled coordinates.") confidence_output = self.confidence_head( - s_inputs=x_inputs.detach(), + single_inputs=single_inputs.detach(), z=z.detach().float(), x_pred=sample_coords.detach(), distogram_atom_idx=disto_idx, @@ -2764,11 +2748,11 @@ class ESMFold2MSAEncoderBlock(nn.Module): def __init__(self, config: ESMFold2Config, is_final_block: bool = False) -> None: super().__init__() me = config.msa_encoder - d_msa = me.d_msa - d_pair = config.d_pair - d_hidden = me.d_hidden + d_msa = me.hidden_size + d_pair = config.pairwise_hidden_size + d_hidden = me.outer_hidden_size n_heads_msa = me.n_heads_msa - msa_head_width = me.msa_head_width + msa_head_width = me.head_width self.is_final_block = is_final_block self.outer_product_mean = ESMFold2OuterProductMean(d_msa, d_hidden, d_pair) if not is_final_block: @@ -2790,19 +2774,19 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def forward( self, - m: Tensor, + msa_repr: Tensor, pair: Tensor, msa_attention_mask: Tensor, pair_attention_mask: Tensor, ) -> tuple[Tensor, Tensor]: - pair = pair + self.outer_product_mean(m, msa_attention_mask) + pair = pair + self.outer_product_mean(msa_repr, msa_attention_mask) if not self.is_final_block: - m = m + self.msa_pair_weighted_averaging(m, pair, pair_attention_mask) - m = self.msa_transition(m) + msa_repr = msa_repr + self.msa_pair_weighted_averaging(msa_repr, pair, pair_attention_mask) + msa_repr = self.msa_transition(msa_repr) pair = pair + self.tri_mul_out(pair, mask=pair_attention_mask) pair = pair + self.tri_mul_in(pair, mask=pair_attention_mask) pair = self.pair_transition(pair) - return m, pair + return msa_repr, pair class ESMFold2MSAEncoder(nn.Module): @@ -2811,8 +2795,8 @@ class ESMFold2MSAEncoder(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() me = config.msa_encoder - d_msa = me.d_msa - d_inputs = config.inputs.d_inputs + d_msa = me.hidden_size + d_inputs = config.inputs.single_inputs_size n_layers = me.n_layers # num_res_types one-hot + has_deletion + deletion_value. self.embed = nn.Linear(config.num_res_types + 2, d_msa, bias=False) @@ -2828,19 +2812,19 @@ def set_chunk_size(self, chunk_size: int | None) -> None: def forward( self, x_pair: Tensor, - x_inputs: Tensor, + single_inputs: Tensor, msa_oh: Tensor, has_deletion: Tensor, deletion_value: Tensor, msa_attention_mask: Tensor, ) -> Tensor: # All inputs are pre-transposed to [B, L, M, ...] before calling. - m_feat = torch.cat([msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1) - m = self.embed(m_feat.to(self.embed.weight.dtype)) + self.project_inputs(x_inputs).unsqueeze(2) + msa_feat = torch.cat([msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)], dim=-1) + msa_repr = self.embed(msa_feat.to(self.embed.weight.dtype)) + self.project_inputs(single_inputs).unsqueeze(2) tok_mask = msa_attention_mask[:, :, 0].bool() pair_attention_mask = tok_mask.unsqueeze(2) & tok_mask.unsqueeze(1) for block in self.blocks: - m, x_pair = block(m, x_pair, msa_attention_mask, pair_attention_mask) + msa_repr, x_pair = block(msa_repr, x_pair, msa_attention_mask, pair_attention_mask) return x_pair From 96b508905b0bb929d1c9bccc7afdf699754a648e Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Jul 2026 14:16:21 +0100 Subject: [PATCH 66/70] Bundle args together, drop some dead args --- .../models/esmfold2/modeling_esmfold2.py | 166 ++++++------------ 1 file changed, 51 insertions(+), 115 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index ff745f5b3307..24760ba4156c 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -34,6 +34,20 @@ from .configuration_esmfold2 import ESMFold2Config +@dataclass +class ESMFold2AtomInputs: + """Reference-conformer atom features, threaded together through the inputs embedder, + atom encoder/decoder, and diffusion module.""" + + ref_pos: Tensor + ref_charge: Tensor + atom_attention_mask: Tensor + ref_element: Tensor + ref_atom_name_chars: Tensor + ref_space_uid: Tensor + atom_to_token: Tensor + + class ESMFold2LayerNorm(nn.LayerNorm): """LayerNorm that always computes in fp32, with its weight stored at the model dtype. @@ -502,49 +516,38 @@ def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> def _compute_step_invariants( self, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, + atom_inputs: ESMFold2AtomInputs, num_diffusion_samples: int, ) -> tuple[Tensor, tuple[Tensor, Tensor, Tensor], Tensor, int]: """Tensors that don't change across diffusion steps (cached per fold): the atom base embedding ``c_base``, 3D-RoPE ``(cos, sin, indices)``, the expanded atom mask, and n_tokens.""" + ref_pos = atom_inputs.ref_pos B, N = ref_pos.shape[:2] atom_feats = torch.cat( [ ref_pos, - ref_charge.unsqueeze(-1), - atom_attention_mask.unsqueeze(-1), - ref_element, - ref_atom_name_chars.reshape(B, N, self.char_feature_dim), + atom_inputs.ref_charge.unsqueeze(-1), + atom_inputs.atom_attention_mask.unsqueeze(-1), + atom_inputs.ref_element, + atom_inputs.ref_atom_name_chars.reshape(B, N, self.char_feature_dim), ], dim=-1, ) c_base = self.atom_norm(self.atom_linear(atom_feats.to(self.atom_linear.weight.dtype)).float()).to( self.atom_linear.weight.dtype ) - cos, sin = self.atom_transformer.rotary_emb(ref_pos, ref_space_uid) + cos, sin = self.atom_transformer.rotary_emb(ref_pos, atom_inputs.ref_space_uid) cos = cos.repeat_interleave(num_diffusion_samples, 0) sin = sin.repeat_interleave(num_diffusion_samples, 0) - mask_exp = atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) + mask_exp = atom_inputs.atom_attention_mask.repeat_interleave(num_diffusion_samples, 0) indices = torch.nonzero(mask_exp.flatten(), as_tuple=False).flatten() attention_params = (cos, sin, indices) - n_tokens = int(atom_to_token.max().item()) + 1 + n_tokens = int(atom_inputs.atom_to_token.max().item()) + 1 return c_base, attention_params, mask_exp, n_tokens def forward( self, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, + atom_inputs: ESMFold2AtomInputs, atom_coords: Tensor | None = None, pred_coords: Tensor | None = None, num_diffusion_samples: int = 1, @@ -561,21 +564,16 @@ def forward( if layer_cache is None or len(layer_cache) == 0: c_base, attention_params, mask_exp, n_tokens = self._compute_step_invariants( - ref_pos=ref_pos, - atom_attention_mask=atom_attention_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=atom_to_token, - num_diffusion_samples=num_diffusion_samples, + atom_inputs, num_diffusion_samples ) if layer_cache is not None: layer_cache["c_base"] = c_base layer_cache["attention_params"] = attention_params layer_cache["mask_exp"] = mask_exp layer_cache["n_tokens"] = n_tokens - layer_cache["atom_to_token_exp"] = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + layer_cache["atom_to_token_exp"] = atom_inputs.atom_to_token.repeat_interleave( + num_diffusion_samples, 0 + ) else: c_base = layer_cache["c_base"] attention_params = layer_cache["attention_params"] @@ -606,7 +604,7 @@ def forward( if layer_cache is not None and "atom_to_token_exp" in layer_cache: atom_to_token_exp = layer_cache["atom_to_token_exp"] else: - atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + atom_to_token_exp = atom_inputs.atom_to_token.repeat_interleave(num_diffusion_samples, 0) token_acts = scatter_atom_to_token(queries_to_acts, atom_to_token_exp, n_tokens, atom_mask=mask_exp.bool()) return token_acts, atom_queries, atom_cond, attention_params @@ -654,12 +652,11 @@ def forward( atom_queries: Tensor, atom_cond: Tensor, atom_pair: tuple, - atom_to_token: Tensor, - atom_attention_mask: Tensor, + atom_inputs: ESMFold2AtomInputs, num_diffusion_samples: int = 1, ) -> Tensor: """Returns coord_update.""" - atom_to_token_exp = atom_to_token.repeat_interleave(num_diffusion_samples, 0) + atom_to_token_exp = atom_inputs.atom_to_token.repeat_interleave(num_diffusion_samples, 0) a_to_q = self.token_to_atom_linear(token_acts) a_to_q = gather_token_to_atom(a_to_q, atom_to_token_exp) atom_queries = atom_queries + a_to_q @@ -974,21 +971,10 @@ def forward( self, x_noisy: Tensor, t_hat: Tensor, - ref_pos: Tensor, - ref_charge: Tensor, - ref_mask: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - tok_idx: Tensor, + atom_inputs: ESMFold2AtomInputs, single_inputs: Tensor, pair_trunk: Tensor, relative_position_encoding: Tensor, - asym_id: Tensor, - residue_index: Tensor, - entity_id: Tensor, - token_index: Tensor, - sym_id: Tensor, sigma_data: float | None = None, token_attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, @@ -1017,13 +1003,7 @@ def forward( # Step 3: atom encoder token_acts, atom_queries_skip, atom_cond_skip, atom_pair_skip = self.atom_encoder( - ref_pos=ref_pos, - atom_attention_mask=ref_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=tok_idx, + atom_inputs, atom_coords=normalized_coords, num_diffusion_samples=num_diffusion_samples, inference_cache=inference_cache, @@ -1051,8 +1031,7 @@ def forward( atom_queries=atom_queries_skip, atom_cond=atom_cond_skip, atom_pair=atom_pair_skip, - atom_to_token=tok_idx, - atom_attention_mask=ref_mask, + atom_inputs=atom_inputs, num_diffusion_samples=num_diffusion_samples, ) @@ -1200,18 +1179,7 @@ def sample( pair_trunk: Tensor, single_inputs: Tensor, relative_position_encoding: Tensor, - ref_pos: Tensor, - ref_charge: Tensor, - ref_mask: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - ref_space_uid: Tensor, - tok_idx: Tensor, - asym_id: Tensor, - residue_index: Tensor, - entity_id: Tensor, - token_index: Tensor, - sym_id: Tensor, + atom_inputs: ESMFold2AtomInputs, token_attention_mask: Tensor | None = None, num_diffusion_samples: int = 1, num_sampling_steps: int | None = None, @@ -1228,7 +1196,7 @@ def sample( so we inflate the underlying schedule length here to land back at the requested step count post-truncation. """ - n_atoms = tok_idx.shape[1] + n_atoms = atom_inputs.atom_to_token.shape[1] device = single_inputs.device target_batch = single_inputs.shape[0] * num_diffusion_samples @@ -1240,7 +1208,7 @@ def sample( eta = self.step_scale if step_scale is None else float(step_scale) x = schedule[0] * torch.randn(target_batch, n_atoms, 3, device=device, dtype=torch.float32) - atom_mask = ref_mask.repeat_interleave(num_diffusion_samples, 0).float() + atom_mask = atom_inputs.atom_attention_mask.repeat_interleave(num_diffusion_samples, 0).float() x_denoised_prev: Tensor | None = None @@ -1257,21 +1225,10 @@ def sample( x_denoised = self.diffusion_module( x_noisy=x_noisy, t_hat=torch.full((target_batch,), t_hat_val, device=device, dtype=torch.float32), - ref_pos=ref_pos, - ref_charge=ref_charge, - ref_mask=ref_mask, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - ref_space_uid=ref_space_uid, - tok_idx=tok_idx, + atom_inputs=atom_inputs, single_inputs=single_inputs, pair_trunk=pair_trunk, relative_position_encoding=relative_position_encoding, - asym_id=asym_id, - residue_index=residue_index, - entity_id=entity_id, - token_index=token_index, - sym_id=sym_id, token_attention_mask=token_attention_mask, num_diffusion_samples=num_diffusion_samples, inference_cache=inference_cache, @@ -1325,13 +1282,7 @@ def forward( aatype: Tensor, profile: Tensor, deletion_mean: Tensor, - ref_pos: Tensor, - atom_attention_mask: Tensor, - ref_space_uid: Tensor, - ref_charge: Tensor, - ref_element: Tensor, - ref_atom_name_chars: Tensor, - atom_to_token: Tensor, + atom_inputs: ESMFold2AtomInputs, ) -> Tensor: """Embed inputs into per-token features. @@ -1339,15 +1290,7 @@ def forward( [B, L, d_inputs] concatenation of atom encoding, aatype, profile, and deletion_mean. """ - a, _q, _c, _attn_params = self.atom_attention_encoder( - ref_pos=ref_pos, - atom_attention_mask=atom_attention_mask, - ref_space_uid=ref_space_uid, - ref_charge=ref_charge, - ref_element=ref_element, - ref_atom_name_chars=ref_atom_name_chars, - atom_to_token=atom_to_token, - ) + a, _q, _c, _attn_params = self.atom_attention_encoder(atom_inputs) # The continuous input features are fp32; fold them into the atom # encoding's (compute) dtype so the single representation is one dtype. dtype = a.dtype @@ -2604,19 +2547,23 @@ def forward( ) ) - single_inputs = self.inputs_embedder( - aatype=res_type_oh, - profile=profile.float(), - deletion_mean=deletion_mean.float(), + atom_inputs = ESMFold2AtomInputs( ref_pos=ref_pos, - atom_attention_mask=atm_mask, - ref_space_uid=ref_space_uid, ref_charge=ref_charge, + atom_attention_mask=atm_mask, ref_element=ref_element_oh, ref_atom_name_chars=ref_atom_name_chars_oh, + ref_space_uid=ref_space_uid, atom_to_token=atom_to_token, ) + single_inputs = self.inputs_embedder( + aatype=res_type_oh, + profile=profile.float(), + deletion_mean=deletion_mean.float(), + atom_inputs=atom_inputs, + ) + z_init = self.z_init_1(single_inputs).unsqueeze(2) + self.z_init_2(single_inputs).unsqueeze(1) relative_position_encoding = self.rel_pos( @@ -2675,18 +2622,7 @@ def forward( pair_trunk=z, single_inputs=single_inputs, relative_position_encoding=relative_position_encoding, - ref_pos=ref_pos, - ref_charge=ref_charge, - ref_mask=atm_mask, - ref_element=ref_element_oh, - ref_atom_name_chars=ref_atom_name_chars_oh, - ref_space_uid=ref_space_uid, - tok_idx=atom_to_token, - asym_id=asym_id, - residue_index=residue_index, - entity_id=entity_id, - token_index=token_index, - sym_id=sym_id, + atom_inputs=atom_inputs, token_attention_mask=tok_mask, num_diffusion_samples=n_samples, num_sampling_steps=num_sampling_steps, From 3faf4711124cbfddab151585039d42a8cdeed674 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Jul 2026 17:51:01 +0100 Subject: [PATCH 67/70] date fixup for CI --- docs/source/en/model_doc/esmc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/en/model_doc/esmc.md b/docs/source/en/model_doc/esmc.md index 424ed3afbd34..feade88ede8f 100644 --- a/docs/source/en/model_doc/esmc.md +++ b/docs/source/en/model_doc/esmc.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-30.* +*This model was contributed to Hugging Face Transformers on 2026-07-02.* # ESMC From 9a48a848d8bf06841b9c00ab77a364d9053e7bd8 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Jul 2026 17:51:04 +0100 Subject: [PATCH 68/70] date fixup for CI --- docs/source/en/model_doc/esmfold2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/en/model_doc/esmfold2.md b/docs/source/en/model_doc/esmfold2.md index f968df2f89e3..791643f9a624 100644 --- a/docs/source/en/model_doc/esmfold2.md +++ b/docs/source/en/model_doc/esmfold2.md @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. rendered properly in your Markdown viewer. --> -*This model was contributed to Hugging Face Transformers on 2026-06-30.* +*This model was contributed to Hugging Face Transformers on 2026-07-02.* # ESMFold2 From 111a3ebc4e540b0944351fe24157ca4cd35c1454 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Jul 2026 13:38:10 +0100 Subject: [PATCH 69/70] Move more stuff to config, merge more swiglus --- .../models/esmfold2/configuration_esmfold2.py | 42 ++++++ .../models/esmfold2/modeling_esmfold2.py | 124 ++++++++++-------- utils/check_config_attributes.py | 7 +- 3 files changed, 110 insertions(+), 63 deletions(-) diff --git a/src/transformers/models/esmfold2/configuration_esmfold2.py b/src/transformers/models/esmfold2/configuration_esmfold2.py index 50c3b4db773c..93bb60704827 100644 --- a/src/transformers/models/esmfold2/configuration_esmfold2.py +++ b/src/transformers/models/esmfold2/configuration_esmfold2.py @@ -43,6 +43,9 @@ class MSAEncoderConfig(PreTrainedConfig): num_hidden_layers: int | None = 4 num_attention_heads: int | None = 8 head_width: int | None = 32 + # SwiGLU transition intermediate size; ESMFold2Config fills it as + # transition_expansion_ratio * hidden_size when not set. + transition_intermediate_size: int | None = None @strict @@ -90,6 +93,14 @@ class AtomAttentionConfig(PreTrainedConfig): n_spatial_rope_pairs_per_axis: int | None = 2 n_uid_rope_pairs: int | None = 10 uid_rope_base_frequency: float | None = 10000.0 + # SwiGLU FFN intermediate size; if not set, expansion_ratio * (atom_hidden_size // 3) * 2 + # rounded up to a multiple of 256 (hardware-aligned width). + ffn_intermediate_size: int | None = None + + def __post_init__(self, **kwargs): + if self.ffn_intermediate_size is None: + self.ffn_intermediate_size = (self.expansion_ratio * (self.atom_hidden_size // 3) * 2 + 255) // 256 * 256 + super().__post_init__(**kwargs) @strict @@ -139,6 +150,20 @@ class DiffusionModuleConfig(PreTrainedConfig): token_num_blocks: int | None = 12 token_num_heads: int | None = 16 transition_multiplier: int | None = 2 + atom_expansion_ratio: int | None = 2 + # SwiGLU intermediate sizes; if not set, atom_ffn = atom_expansion_ratio * (atom_hidden_size // 3) * 2 + # rounded up to a multiple of 256, and token_transition = transition_multiplier * token_hidden_size. + atom_ffn_intermediate_size: int | None = None + token_transition_intermediate_size: int | None = None + + def __post_init__(self, **kwargs): + if self.atom_ffn_intermediate_size is None: + self.atom_ffn_intermediate_size = ( + (self.atom_expansion_ratio * (self.atom_hidden_size // 3) * 2 + 255) // 256 * 256 + ) + if self.token_transition_intermediate_size is None: + self.token_transition_intermediate_size = self.transition_multiplier * self.token_hidden_size + super().__post_init__(**kwargs) @strict @@ -202,6 +227,12 @@ class ESMFold2Config(PreTrainedConfig): Dimensionality of single (per-residue) representations. pairwise_hidden_size (`int`, *optional*, defaults to 256): Dimensionality of pair (residue-residue) representations. + transition_expansion_ratio (`int`, *optional*, defaults to 4): + Expansion ratio for the pair- and MSA-stream SwiGLU transition FFNs (matches the + reference ESMFold2 feed-forward blocks). + pair_transition_intermediate_size (`int`, *optional*): + Hidden size of the pair-stream SwiGLU transitions (folding trunk, LM encoder, parcae + coda and MSA encoder). Derived as `transition_expansion_ratio * pairwise_hidden_size` if not set. n_relative_residx_bins (`int`, *optional*, defaults to 32): Number of bins for relative residue index encoding. n_relative_chain_bins (`int`, *optional*, defaults to 2): @@ -274,6 +305,8 @@ class ESMFold2Config(PreTrainedConfig): type: str | None = "release" hidden_size: int | None = 384 pairwise_hidden_size: int | None = 256 + transition_expansion_ratio: int | None = 4 + pair_transition_intermediate_size: int | None = None n_relative_residx_bins: int | None = 32 n_relative_chain_bins: int | None = 2 num_loops: int | None = 10 @@ -315,6 +348,15 @@ def _init_nested(cls, val): self.lm_encoder = _init_nested(LMEncoderConfig, self.lm_encoder) self.esmc_config = _init_nested(ESMCConfig, self.esmc_config) + # Pair- and MSA-stream SwiGLU transitions size their FFN as transition_expansion_ratio times + # the respective stream width (matches the reference ESMFold2 feed-forward blocks). + if self.pair_transition_intermediate_size is None: + self.pair_transition_intermediate_size = self.transition_expansion_ratio * self.pairwise_hidden_size + if self.msa_encoder.transition_intermediate_size is None: + self.msa_encoder.transition_intermediate_size = ( + self.transition_expansion_ratio * self.msa_encoder.hidden_size + ) + super().__post_init__(**kwargs) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 24760ba4156c..257ff4a87787 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -121,44 +121,32 @@ def forward(self, t_hat: Tensor) -> Tensor: class ESMFold2SwiGLU(nn.Module): - """ESMFold2SwiGLU with packed w12 and output w3.""" + """SwiGLU feed-forward with a packed w12 (fused gate+up) projection and output w3. + + ``intermediate_size`` is supplied by the caller and registered on the config, so every + ESMFold2 SwiGLU feed-forward is this one module regardless of how its width is derived. + """ def __init__( self, in_features: int, - hidden_features: int, + intermediate_size: int, out_features: int | None = None, - bias: bool = True, + bias: bool = False, ) -> None: super().__init__() out_features = out_features or in_features - self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) - self.w3 = nn.Linear(hidden_features, out_features, bias=bias) - self.hidden_features = hidden_features + self.w12 = nn.Linear(in_features, 2 * intermediate_size, bias=bias) + self.w3 = nn.Linear(intermediate_size, out_features, bias=bias) + self.intermediate_size = intermediate_size def forward(self, x: Tensor) -> Tensor: gate_up = self.w12(x) - gate, up = gate_up.split(self.hidden_features, dim=-1) + gate, up = gate_up.split(self.intermediate_size, dim=-1) hidden = F.silu(gate) * up return self.w3(hidden) -class ESMFold2SwiGLUMLP(ESMFold2SwiGLU): - """ESMFold2SwiGLU MLP with packed weights, no bias.""" - - def __init__(self, d_model: int, expansion_ratio: int = 4, bias: bool = False) -> None: - hidden = expansion_ratio * d_model - super().__init__(in_features=d_model, hidden_features=hidden, out_features=d_model, bias=bias) - - -class ESMFold2SwiGLUFFN(ESMFold2SwiGLU): - """ESMFold2SwiGLU FFN with rounded hidden size for hardware alignment.""" - - def __init__(self, d_model: int, expansion_ratio: int = 2) -> None: - hidden_size = ((expansion_ratio * (d_model // 3) * 2) + 255) // 256 * 256 - super().__init__(in_features=d_model, hidden_features=hidden_size, out_features=d_model, bias=False) - - # Copied from transformers.models.llama.modeling_llama.rotate_half def rotate_half(x): """Rotates half the hidden dims of the input.""" @@ -228,25 +216,38 @@ def _resolve_atom_config(config: ESMFold2Config, structure_prediction: bool): The atom transformer is the same architecture in two places: the inputs embedder (``structure_prediction=False``) and the diffusion module - (``structure_prediction=True``). Its I/O dims and block/head counts come - from the call-site sub-config; the window, expansion ratio and 3D-RoPE - settings always come from ``config.inputs.atom_encoder`` — the diffusion - module reused those (its atom encoder was built with the same window/RoPE and - a fixed ``expansion_ratio=2``, which is ``AtomAttentionConfig``'s default). + (``structure_prediction=True``). Its I/O dims, block/head counts and FFN width + come from the call-site sub-config; the window and 3D-RoPE settings always come + from ``config.inputs.atom_encoder`` — the diffusion module reused those (its atom + encoder was built with the same window/RoPE). Every module in the atom stack (attention/block/transformer/encoder/decoder) takes only ``(config, structure_prediction)`` and derives its own dims from this, so no scalar dims are threaded between them. - Returns ``(d_atom, d_token, n_blocks, n_heads, swa)`` where ``swa`` is the - ``AtomAttentionConfig`` carrying ``swa_window_size`` / ``expansion_ratio`` / - the RoPE settings. + Returns ``(d_atom, d_token, n_blocks, n_heads, swa, ffn_intermediate_size)`` where ``swa`` + is the ``AtomAttentionConfig`` carrying ``swa_window_size`` / the RoPE settings and + ``ffn_intermediate_size`` is the SwiGLU FFN width for this call site's atom stack. """ swa = config.inputs.atom_encoder if structure_prediction: dm = config.structure_head.diffusion_module - return dm.atom_hidden_size, dm.token_hidden_size, dm.atom_num_blocks, dm.atom_num_heads, swa - return swa.atom_hidden_size, swa.token_hidden_size, swa.n_blocks, swa.n_heads, swa + return ( + dm.atom_hidden_size, + dm.token_hidden_size, + dm.atom_num_blocks, + dm.atom_num_heads, + swa, + dm.atom_ffn_intermediate_size, + ) + return ( + swa.atom_hidden_size, + swa.token_hidden_size, + swa.n_blocks, + swa.n_heads, + swa, + swa.ffn_intermediate_size, + ) def _swa_window_mask_function(rank: Tensor, valid: Tensor, half_window: int) -> Callable: @@ -278,7 +279,7 @@ class ESMFold2SWA3DRoPEAttention(nn.Module): def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - d_model, _d_token, _n_blocks, n_heads, swa = _resolve_atom_config(config, structure_prediction) + d_model, _d_token, _n_blocks, n_heads, swa, _ffn = _resolve_atom_config(config, structure_prediction) self.config = config self.n_heads = n_heads self.head_dim = d_model // n_heads @@ -362,12 +363,14 @@ class ESMFold2SWAAtomBlock(nn.Module): def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - d_atom, _d_token, _n_blocks, _n_heads, swa = _resolve_atom_config(config, structure_prediction) + d_atom, _d_token, _n_blocks, _n_heads, _swa, ffn_intermediate_size = _resolve_atom_config( + config, structure_prediction + ) # adaln-Zero gate; zero-init lives in ESMFold2PreTrainedModel._init_weights. self.adaln_linear = nn.Linear(d_atom, 6 * d_atom, bias=False) self.attn = ESMFold2SWA3DRoPEAttention(config, structure_prediction) - self.ffn = ESMFold2SwiGLUFFN(d_atom, swa.expansion_ratio) + self.ffn = ESMFold2SwiGLU(d_atom, ffn_intermediate_size, d_atom, bias=False) def forward(self, x: Tensor, atom_cond: Tensor, attention_params: tuple) -> Tensor: modulation = self.adaln_linear(F.silu(atom_cond)) @@ -397,7 +400,7 @@ class ESMFold2RotaryEmbedding3D(nn.Module): def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - d_atom, _d_token, _n_blocks, n_heads, swa = _resolve_atom_config(config, structure_prediction) + d_atom, _d_token, _n_blocks, n_heads, swa, _ffn = _resolve_atom_config(config, structure_prediction) self.head_dim = d_atom // n_heads self.n_spatial_per_axis = swa.n_spatial_rope_pairs_per_axis self.n_uid_pairs = swa.n_uid_rope_pairs @@ -437,7 +440,7 @@ class ESMFold2SWAAtomTransformer(nn.Module): def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - _d_atom, _d_token, n_blocks, _n_heads, swa = _resolve_atom_config(config, structure_prediction) + _d_atom, _d_token, n_blocks, _n_heads, swa, _ffn = _resolve_atom_config(config, structure_prediction) self.swa_window_size = swa.swa_window_size self.rotary_emb = ESMFold2RotaryEmbedding3D(config, structure_prediction) @@ -494,7 +497,7 @@ class ESMFold2AtomEncoder(nn.Module): def __init__(self, config: ESMFold2Config, structure_prediction: bool = True) -> None: super().__init__() - d_atom, d_token, _n_blocks, _n_heads, _swa = _resolve_atom_config(config, structure_prediction) + d_atom, d_token, _n_blocks, _n_heads, _swa, _ffn = _resolve_atom_config(config, structure_prediction) self.d_atom = d_atom self.d_token = d_token self.structure_prediction = structure_prediction @@ -638,7 +641,7 @@ class ESMFold2AtomDecoder(nn.Module): def __init__(self, config: ESMFold2Config) -> None: super().__init__() - d_atom, d_token, _n_blocks, _n_heads, _swa = _resolve_atom_config(config, structure_prediction=True) + d_atom, d_token, _n_blocks, _n_heads, _swa, _ffn = _resolve_atom_config(config, structure_prediction=True) self.token_to_atom_linear = nn.Linear(d_token, d_atom, bias=False) self.atom_transformer = ESMFold2SWAAtomTransformer(config, structure_prediction=True) @@ -766,18 +769,17 @@ class ESMFold2ConditionedTransitionBlock(nn.Module): def __init__( self, d_model: int, + intermediate_size: int, d_cond: int | None = None, - transition_multiplier: int = 2, ) -> None: super().__init__() d_cond = d_cond or d_model - hidden = transition_multiplier * d_model self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. self.output_gate = nn.Linear(d_cond, d_model, bias=True) - self.ffn = ESMFold2SwiGLU(d_model, hidden, d_model, bias=False) + self.ffn = ESMFold2SwiGLU(d_model, intermediate_size, d_model, bias=False) def forward(self, token_acts: Tensor, single_repr: Tensor) -> Tensor: x = self.adaln(token_acts, single_repr) @@ -796,7 +798,7 @@ def __init__(self, config: ESMFold2Config) -> None: num_heads = dm.token_num_heads num_blocks = dm.token_num_blocks d_cond = dm.token_hidden_size - transition_multiplier = dm.transition_multiplier + transition_intermediate_size = dm.token_transition_intermediate_size self.attn_blocks = nn.ModuleList( [ @@ -813,8 +815,8 @@ def __init__(self, config: ESMFold2Config) -> None: [ ESMFold2ConditionedTransitionBlock( d_model=d_model, + intermediate_size=transition_intermediate_size, d_cond=d_cond, - transition_multiplier=transition_multiplier, ) for _ in range(num_blocks) ] @@ -1528,10 +1530,10 @@ def forward(self, z: Tensor, mask: Tensor | None = None) -> Tensor: class ESMFold2Transition(nn.Module): """LayerNorm + ESMFold2SwiGLU feed-forward residual block, chunked along the token axis.""" - def __init__(self, d_model: int, expansion_ratio: int = 4) -> None: + def __init__(self, d_model: int, intermediate_size: int) -> None: super().__init__() self.norm = ESMFold2LayerNorm(d_model) - self.ffn = ESMFold2SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False) + self.ffn = ESMFold2SwiGLU(d_model, intermediate_size, d_model, bias=False) # Default chunked; set_chunk_size(None) disables. self._chunk_size: int | None = _DEFAULT_CHUNK_SIZE @@ -1552,11 +1554,11 @@ def forward(self, x: Tensor) -> Tensor: class ESMFold2PairUpdateBlock(nn.Module): """tri_mul_out, tri_mul_in, pair_transition.""" - def __init__(self, d_pair: int = 256, expansion_ratio: int = 4) -> None: + def __init__(self, d_pair: int, intermediate_size: int) -> None: super().__init__() self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=True) self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=False) - self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=expansion_ratio) + self.pair_transition = ESMFold2Transition(d_pair, intermediate_size) def set_chunk_size(self, chunk_size: int | None) -> None: self.tri_mul_out.set_chunk_size(chunk_size) @@ -1574,10 +1576,10 @@ def forward(self, pair: Tensor, pair_attention_mask: Tensor | None = None) -> Te class ESMFold2FoldingTrunk(nn.Module): """ModuleList of PairUpdateBlocks.""" - def __init__(self, n_layers: int = 24, d_pair: int = 256, expansion_ratio: int = 4) -> None: + def __init__(self, n_layers: int, d_pair: int, intermediate_size: int) -> None: super().__init__() self.blocks = nn.ModuleList( - [ESMFold2PairUpdateBlock(d_pair=d_pair, expansion_ratio=expansion_ratio) for _ in range(n_layers)] + [ESMFold2PairUpdateBlock(d_pair=d_pair, intermediate_size=intermediate_size) for _ in range(n_layers)] ) def set_chunk_size(self, chunk_size: int | None) -> None: @@ -1820,7 +1822,9 @@ def __init__(self, config: ESMFold2Config) -> None: self.row_attention_pooling = ESMFold2RowAttentionPooling(d_pair=d_pair, d_single=d_single) pf = ch.folding_trunk - self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) + self.folding_trunk = ESMFold2FoldingTrunk( + n_layers=pf.n_layers, d_pair=d_pair, intermediate_size=config.pair_transition_intermediate_size + ) # Heads. self.plddt_ln = ESMFold2LayerNorm(d_single) @@ -2280,10 +2284,14 @@ def __init__(self, config: ESMFold2Config) -> None: self.esmc = AutoModel.from_config(config.esmc_config) pf = config.folding_trunk - self.folding_trunk = ESMFold2FoldingTrunk(n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4) + self.folding_trunk = ESMFold2FoldingTrunk( + n_layers=pf.n_layers, d_pair=d_pair, intermediate_size=config.pair_transition_intermediate_size + ) if config.lm_encoder.enabled: self.lm_encoder: ESMFold2FoldingTrunk | None = ESMFold2FoldingTrunk( - n_layers=config.lm_encoder.n_layers, d_pair=d_pair, expansion_ratio=4 + n_layers=config.lm_encoder.n_layers, + d_pair=d_pair, + intermediate_size=config.pair_transition_intermediate_size, ) else: self.lm_encoder = None @@ -2296,7 +2304,9 @@ def __init__(self, config: ESMFold2Config) -> None: self.parcae_b_cont = nn.Parameter(torch.empty(d_pair, d_pair)) self.parcae_readout = nn.Linear(d_pair, d_pair, bias=False) self.parcae_coda = ESMFold2FoldingTrunk( - n_layers=config.parcae.num_coda_layers, d_pair=d_pair, expansion_ratio=4 + n_layers=config.parcae.num_coda_layers, + d_pair=d_pair, + intermediate_size=config.pair_transition_intermediate_size, ) # Heads -------------------------------------------------------------- @@ -2695,10 +2705,10 @@ def __init__(self, config: ESMFold2Config, is_final_block: bool = False) -> None self.msa_pair_weighted_averaging = ESMFold2MSAPairWeightedAveraging( d_msa, d_pair, n_heads_msa, msa_head_width ) - self.msa_transition = ESMFold2Transition(d_msa, expansion_ratio=4) + self.msa_transition = ESMFold2Transition(d_msa, me.transition_intermediate_size) self.tri_mul_out = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=True) self.tri_mul_in = ESMFold2TriangleMultiplicativeUpdate(dim=d_pair, outgoing=False) - self.pair_transition = ESMFold2Transition(d_pair, expansion_ratio=4) + self.pair_transition = ESMFold2Transition(d_pair, config.pair_transition_intermediate_size) def set_chunk_size(self, chunk_size: int | None) -> None: self.outer_product_mean.set_chunk_size(chunk_size) diff --git a/utils/check_config_attributes.py b/utils/check_config_attributes.py index 60fd851c1b41..c1d430531ddb 100644 --- a/utils/check_config_attributes.py +++ b/utils/check_config_attributes.py @@ -147,12 +147,7 @@ "GlmMoeDsaConfig": ["head_dim", "layer_types", "mlp_bias", "first_k_dense_replace", "n_routed_experts"], "EsmFoldConfig": ["esm_ablate_pairwise", "esm_ablate_sequence", "esm_input_dropout", "esm_type"], "TrunkConfig": ["cpu_grad_checkpoint", "layer_drop"], - # type: architecture-variant marker (validated, "release"-only), read in - # __post_init__ which the checker can't scan. - "ESMFold2Config": ["type"], - # ESMFold2 sub-configs: their fields are threaded into submodules as explicit - # dims (e.g. ESMFold2AtomEncoder(d_atom=cfg.inputs.atom_encoder.d_atom, ...)), - # never read as `config.`, so the checker's heuristic can't trace them. + "ESMFold2Config": ["type", "transition_expansion_ratio"], "AtomAttentionConfig": True, "ConfidenceHeadConfig": True, "DiffusionModuleConfig": True, From 78df1b0b203ef0fcd5b8609256354f662f123d85 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Jul 2026 14:42:23 +0100 Subject: [PATCH 70/70] Cleaning up some constants --- .../models/esmfold2/modeling_esmfold2.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/transformers/models/esmfold2/modeling_esmfold2.py b/src/transformers/models/esmfold2/modeling_esmfold2.py index 257ff4a87787..f2f36e1f45d0 100644 --- a/src/transformers/models/esmfold2/modeling_esmfold2.py +++ b/src/transformers/models/esmfold2/modeling_esmfold2.py @@ -691,7 +691,7 @@ def __init__( self.scale = self.head_dim**-0.5 d_cond = d_cond or d_model - self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond) # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. self.out_gate = nn.Linear(d_cond, d_model, bias=True) @@ -701,7 +701,7 @@ def __init__( self.out_proj = nn.Linear(d_model, d_model, bias=False) if d_pair > 0: - self.pair_norm = ESMFold2LayerNorm(d_pair, eps=1e-5) + self.pair_norm = ESMFold2LayerNorm(d_pair) self.pair_bias_proj = nn.Linear(d_pair, num_heads, bias=False) def compute_pair_bias(self, pair_repr: Tensor, bsz: int, num_diffusion_samples: int = 1) -> Tensor: @@ -775,7 +775,7 @@ def __init__( super().__init__() d_cond = d_cond or d_model - self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond, eps=1e-5) + self.adaln = ESMFold2AdaptiveLayerNorm(d_model, d_cond) # adaln-Zero gate (weight 0, bias -2); init in ESMFold2PreTrainedModel._init_weights. self.output_gate = nn.Linear(d_cond, d_model, bias=True) @@ -1436,8 +1436,6 @@ def forward(self, hidden_states: Tensor) -> Tensor: return lm_z -_EPS = 1e-5 - _DEFAULT_CHUNK_SIZE = 64 @@ -1462,8 +1460,8 @@ def __init__(self, input_channels: int, latent_channels: int, flow: str) -> None self.latent_channels = latent_channels self.flow = flow self._einsum_equation = self._FLOW_TO_EINSUM[flow] - self.norm_start = ESMFold2LayerNorm(self.input_channels, eps=_EPS) - self.norm_mix = ESMFold2LayerNorm(self.latent_channels, eps=_EPS) + self.norm_start = ESMFold2LayerNorm(self.input_channels) + self.norm_mix = ESMFold2LayerNorm(self.latent_channels) self.proj_bundle = nn.Linear(self.input_channels, 4 * self.latent_channels, bias=False) self.proj_emit = nn.Linear(self.latent_channels, self.input_channels, bias=False) self.proj_gate = nn.Linear(self.input_channels, self.input_channels, bias=False)