lhallee commited on
Commit
41f44be
·
verified ·
1 Parent(s): 85be70c

Upload modeling_esm_plusplus.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. modeling_esm_plusplus.py +1011 -14
modeling_esm_plusplus.py CHANGED
@@ -1,4 +1,1015 @@
1
  from __future__ import annotations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
  ESM++ model implementation.
4
 
@@ -28,20 +1039,6 @@ from tokenizers.processors import TemplateProcessing
28
  from transformers import PreTrainedModel, PreTrainedTokenizerFast, PretrainedConfig
29
  from transformers.modeling_outputs import ModelOutput
30
 
31
- try:
32
- from fastplms.attention import (
33
- AttentionBackend, VALID_ATTENTION_BACKENDS,
34
- resolve_attention_backend, get_attention_mask,
35
- _get_flex_attention_fn,
36
- _ensure_flash_kernels_loaded, FLASH_KERNEL, FLASH_KERNEL_VARIANT,
37
- _kernels_flash_forward, _kernels_flash_varlen_forward,
38
- kernels_flash_attention_func,
39
- index_first_axis, index_put_first_axis, pad_input, _unpad_input,
40
- create_block_mask, flex_attention, BlockMask,
41
- )
42
- from fastplms.embedding_mixin import Pooler, EmbeddingMixin, ProteinDataset, parse_fasta, build_collator
43
- except ImportError:
44
- pass # Running as HF Hub composite; shared definitions are above
45
 
46
 
47
  class ESMplusplusConfig(PretrainedConfig):
 
1
  from __future__ import annotations
2
+
3
+ import torch
4
+ import torch._inductor.config as inductor_config
5
+ import torch._dynamo as dynamo
6
+
7
+ # Enable TensorFloat32 tensor cores for float32 matmul (Ampere+ GPUs)
8
+ # Provides significant speedup with minimal precision loss
9
+ torch.set_float32_matmul_precision('high')
10
+
11
+ # Enable TF32 for matrix multiplications and cuDNN operations
12
+ torch.backends.cuda.matmul.allow_tf32 = True
13
+ torch.backends.cudnn.allow_tf32 = True
14
+
15
+ # Enable cuDNN autotuner - finds fastest algorithms for your hardware
16
+ # Best when input sizes are consistent; may slow down first iterations
17
+ torch.backends.cudnn.benchmark = True
18
+
19
+ # Deterministic operations off for speed (set True if reproducibility needed)
20
+ torch.backends.cudnn.deterministic = False
21
+ inductor_config.max_autotune_gemm_backends = "ATEN,CUTLASS,FBGEMM"
22
+
23
+ dynamo.config.capture_scalar_outputs = True
24
+ torch._dynamo.config.recompile_limit = 16
25
+
26
+ import io
27
+ import os
28
+ import queue
29
+ import sqlite3
30
+ import struct
31
+ import threading
32
+ import time
33
+
34
+ import networkx as nx
35
+ import numpy as np
36
+ import torch
37
+ from tqdm.auto import tqdm
38
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple
39
+ from torch.utils.data import DataLoader
40
+ from torch.utils.data import Dataset as TorchDataset
41
+ from transformers import PreTrainedTokenizerBase
42
+
43
+
44
+ # Compact blob serialization constants
45
+ # Canonical source: core/embed/blob.py. Keep in sync with protify/utils.py.
46
+ _COMPACT_VERSION = 0x01
47
+ _DTYPE_TO_CODE = {torch.float16: 0, torch.bfloat16: 1, torch.float32: 2}
48
+ _CODE_TO_DTYPE = {0: torch.float16, 1: torch.bfloat16, 2: torch.float32}
49
+ _CODE_TO_NP_DTYPE = {0: np.float16, 1: np.float16, 2: np.float32}
50
+
51
+
52
+ def tensor_to_embedding_blob(tensor: torch.Tensor) -> bytes:
53
+ """Serialize a tensor to compact binary format for SQLite blob storage.
54
+
55
+ Format: [version:1][dtype_code:1][ndim:4][shape:4*ndim][raw_bytes]
56
+ bfloat16 tensors are stored as float16 bytes (numpy lacks bfloat16)
57
+ but tagged with dtype_code=1 so they can be cast back on read.
58
+ Falls back to torch.save for unsupported dtypes.
59
+ """
60
+ t = tensor.cpu()
61
+ if t.dtype not in _DTYPE_TO_CODE:
62
+ buffer = io.BytesIO()
63
+ torch.save(t, buffer)
64
+ return buffer.getvalue()
65
+ dtype_code = _DTYPE_TO_CODE[t.dtype]
66
+
67
+ if t.dtype == torch.bfloat16:
68
+ raw = t.half().numpy().tobytes()
69
+ else:
70
+ raw = t.numpy().tobytes()
71
+
72
+ shape = t.shape
73
+ header = struct.pack(f'<BBi{len(shape)}i', _COMPACT_VERSION, dtype_code, len(shape), *shape)
74
+ return header + raw
75
+
76
+
77
+ def _compact_header(dtype: torch.dtype, shape: tuple) -> bytes:
78
+ """Build just the compact header for a given dtype and shape."""
79
+ dtype_code = _DTYPE_TO_CODE[dtype]
80
+ return struct.pack(f'<BBi{len(shape)}i', _COMPACT_VERSION, dtype_code, len(shape), *shape)
81
+
82
+
83
+ def batch_tensor_to_blobs(batch: torch.Tensor) -> List[bytes]:
84
+ """Serialize a batch of same-shape tensors to compact blobs (fast path for vectors).
85
+
86
+ Builds the header once and slices raw bytes per row. Much faster than
87
+ per-row tensor_to_embedding_blob calls for uniform-shape batches.
88
+ """
89
+ assert batch.ndim >= 2, f"Expected batch with >= 2 dims, got {batch.ndim}"
90
+ t = batch.cpu()
91
+ store_dtype = t.dtype
92
+ if t.dtype not in _DTYPE_TO_CODE:
93
+ return [tensor_to_embedding_blob(t[i]) for i in range(t.shape[0])]
94
+
95
+ if t.dtype == torch.bfloat16:
96
+ arr = t.half().numpy()
97
+ store_dtype = torch.bfloat16
98
+ else:
99
+ arr = t.numpy()
100
+
101
+ row_shape = tuple(t.shape[1:])
102
+ header = _compact_header(store_dtype, row_shape)
103
+ raw = arr.tobytes()
104
+ stride = len(raw) // t.shape[0]
105
+ return [header + raw[i * stride:(i + 1) * stride] for i in range(t.shape[0])]
106
+
107
+
108
+ def embedding_blob_to_tensor(blob: bytes, fallback_shape: Optional[Tuple[int, ...]] = None) -> torch.Tensor:
109
+ """Deserialize a blob back to a tensor. Auto-detects compact vs legacy formats."""
110
+ if len(blob) >= 6 and blob[0] == _COMPACT_VERSION:
111
+ dtype_code = blob[1]
112
+ ndim = struct.unpack_from('<i', blob, 2)[0]
113
+ shape = struct.unpack_from(f'<{ndim}i', blob, 6)
114
+ data_offset = 6 + 4 * ndim
115
+ np_dtype = _CODE_TO_NP_DTYPE[dtype_code]
116
+ arr = np.frombuffer(blob, dtype=np_dtype, offset=data_offset).copy().reshape(shape)
117
+ t = torch.from_numpy(arr)
118
+ target_dtype = _CODE_TO_DTYPE[dtype_code]
119
+ if target_dtype != t.dtype:
120
+ t = t.to(target_dtype)
121
+ return t
122
+
123
+ # Fallback: try torch.load (pickle format)
124
+ try:
125
+ buffer = io.BytesIO(blob)
126
+ return torch.load(buffer, map_location='cpu', weights_only=True)
127
+ except Exception:
128
+ pass
129
+
130
+ # Legacy fallback: raw float32 bytes with caller-supplied shape
131
+ assert fallback_shape is not None, "Cannot deserialize blob: unknown format and no fallback_shape provided."
132
+ arr = np.frombuffer(blob, dtype=np.float32).copy().reshape(fallback_shape)
133
+ return torch.from_numpy(arr)
134
+
135
+
136
+ def maybe_compile(model: torch.nn.Module, dynamic: bool = False) -> torch.nn.Module:
137
+ """Compile model with torch.compile if possible.
138
+
139
+ Skips compilation when dynamic=True (padding='longest') because
140
+ flex attention's create_block_mask is incompatible with dynamic shapes
141
+ under torch.compile, causing CUDA illegal memory access.
142
+ """
143
+ if dynamic:
144
+ print("Skipping torch.compile (dynamic shapes + flex attention incompatible)")
145
+ return model
146
+ try:
147
+ model = torch.compile(model)
148
+ print("Model compiled")
149
+ except Exception as e:
150
+ print(f"Skipping torch.compile: {e}")
151
+ return model
152
+
153
+
154
+ def build_collator(
155
+ tokenizer: PreTrainedTokenizerBase,
156
+ padding: str = 'max_length',
157
+ max_length: int = 512,
158
+ ) -> Callable[[List[str]], Dict[str, torch.Tensor]]:
159
+ def _collate_fn(sequences: List[str]) -> Dict[str, torch.Tensor]:
160
+ kwargs: Dict[str, Any] = dict(
161
+ return_tensors="pt", padding=padding, truncation=True, max_length=max_length,
162
+ )
163
+ if padding != 'max_length':
164
+ kwargs['pad_to_multiple_of'] = 8
165
+ return tokenizer(sequences, **kwargs)
166
+ return _collate_fn
167
+
168
+
169
+ def _make_embedding_progress(
170
+ dataloader: DataLoader,
171
+ padding: str,
172
+ n_warmup: int = 3,
173
+ n_calibration: int = 5,
174
+ ) -> Iterator[Tuple[int, Any]]:
175
+ """Progress-bar wrapper for embedding loops. Drop-in replacement for enumerate(dataloader).
176
+
177
+ When padding='max_length', all batches have uniform cost so plain tqdm works.
178
+ When padding='longest' (sorted longest-first), batch times vary dramatically.
179
+ In that case: yield warmup batches first (compiler warmup + OOM check on longest
180
+ sequences), then time mid-length calibration batches to estimate total ETA.
181
+
182
+ Keep in sync with protify/embedder.py and core/atlas/precomputed.py.
183
+ """
184
+ total = len(dataloader)
185
+ if padding == 'max_length' or total <= n_warmup + n_calibration:
186
+ for i, batch in tqdm(enumerate(dataloader), total=total, desc='Embedding batches'):
187
+ yield i, batch
188
+ return
189
+
190
+ dl_iter = iter(dataloader)
191
+
192
+ # Phase 1: warmup on longest batches (first n_warmup, since sorted longest-first)
193
+ warmup_bar = tqdm(range(n_warmup), desc='Warmup (longest batches)', leave=False)
194
+ for i in warmup_bar:
195
+ batch = next(dl_iter)
196
+ yield i, batch
197
+ warmup_bar.close()
198
+
199
+ # Phase 2: skip to middle of dataset for calibration timing
200
+ # We need to yield all intermediate batches too (they contain real data)
201
+ mid_start = total // 2
202
+ intermediate_bar = tqdm(
203
+ range(n_warmup, mid_start), desc='Embedding batches', leave=False,
204
+ )
205
+ for i in intermediate_bar:
206
+ batch = next(dl_iter)
207
+ yield i, batch
208
+ intermediate_bar.close()
209
+
210
+ # Phase 3: time calibration batches from the middle
211
+ calibration_times: List[float] = []
212
+ cal_bar = tqdm(range(n_calibration), desc='Calibrating ETA', leave=False)
213
+ for j in cal_bar:
214
+ t0 = time.perf_counter()
215
+ batch = next(dl_iter)
216
+ yield mid_start + j, batch
217
+ calibration_times.append(time.perf_counter() - t0)
218
+ cal_bar.close()
219
+
220
+ avg_time = sum(calibration_times) / len(calibration_times)
221
+ remaining_start = mid_start + n_calibration
222
+ remaining_count = total - remaining_start
223
+ estimated_total_seconds = avg_time * remaining_count
224
+
225
+ # Phase 4: remaining batches with calibrated ETA
226
+ main_bar = tqdm(
227
+ range(remaining_count),
228
+ desc='Embedding batches',
229
+ bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]',
230
+ )
231
+ main_bar.set_postfix_str(f'ETA ~{estimated_total_seconds:.0f}s (calibrated)')
232
+ for k in main_bar:
233
+ batch = next(dl_iter)
234
+ yield remaining_start + k, batch
235
+ main_bar.close()
236
+
237
+
238
+ class _SQLWriter:
239
+ """Context manager for async SQL embedding writes. Matches core/embed/storage.SQLEmbeddingWriter."""
240
+
241
+ def __init__(self, conn: sqlite3.Connection, queue_maxsize: int = 4) -> None:
242
+ self._conn = conn
243
+ self._queue: queue.Queue = queue.Queue(maxsize=queue_maxsize)
244
+ self._thread: Optional[threading.Thread] = None
245
+
246
+ def __enter__(self) -> "_SQLWriter":
247
+ self._thread = threading.Thread(target=self._writer_loop, daemon=True)
248
+ self._thread.start()
249
+ return self
250
+
251
+ def write_batch(self, rows: List[Tuple[str, bytes]]) -> None:
252
+ self._queue.put(rows)
253
+
254
+ def _writer_loop(self) -> None:
255
+ cursor = self._conn.cursor()
256
+ while True:
257
+ item = self._queue.get()
258
+ if item is None:
259
+ break
260
+ cursor.executemany("INSERT OR REPLACE INTO embeddings VALUES (?, ?)", item)
261
+ if self._queue.qsize() == 0:
262
+ self._conn.commit()
263
+ self._conn.commit()
264
+
265
+ def __exit__(self, *exc) -> None:
266
+ if self._thread is not None:
267
+ self._queue.put(None)
268
+ self._thread.join()
269
+ self._thread = None
270
+
271
+
272
+ class Pooler:
273
+ def __init__(self, pooling_types: List[str]) -> None:
274
+ self.pooling_types = pooling_types
275
+ self.pooling_options: Dict[str, Callable] = {
276
+ 'mean': self.mean_pooling,
277
+ 'max': self.max_pooling,
278
+ 'norm': self.norm_pooling,
279
+ 'median': self.median_pooling,
280
+ 'std': self.std_pooling,
281
+ 'var': self.var_pooling,
282
+ 'cls': self.cls_pooling,
283
+ 'parti': self._pool_parti,
284
+ }
285
+
286
+ def _create_pooled_matrices_across_layers(self, attentions: torch.Tensor) -> torch.Tensor:
287
+ assert isinstance(attentions, torch.Tensor)
288
+ maxed_attentions = torch.max(attentions, dim=1)[0]
289
+ return maxed_attentions
290
+
291
+ def _page_rank(self, attention_matrix: np.ndarray, personalization: Optional[dict] = None, nstart: Optional[dict] = None, prune_type: str = "top_k_outdegree") -> Dict[int, float]:
292
+ G = self._convert_to_graph(attention_matrix)
293
+ if G.number_of_nodes() != attention_matrix.shape[0]:
294
+ raise Exception(
295
+ f"The number of nodes in the graph should be equal to the number of tokens in sequence! You have {G.number_of_nodes()} nodes for {attention_matrix.shape[0]} tokens.")
296
+ if G.number_of_edges() == 0:
297
+ raise Exception(f"You don't seem to have any attention edges left in the graph.")
298
+
299
+ return nx.pagerank(G, alpha=0.85, tol=1e-06, weight='weight', personalization=personalization, nstart=nstart, max_iter=100)
300
+
301
+ def _convert_to_graph(self, matrix: np.ndarray) -> nx.DiGraph:
302
+ G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
303
+ return G
304
+
305
+ def _calculate_importance_weights(self, dict_importance: Dict[int, float], attention_mask: Optional[torch.Tensor] = None) -> np.ndarray:
306
+ if attention_mask is not None:
307
+ for k in list(dict_importance.keys()):
308
+ if attention_mask[k] == 0:
309
+ del dict_importance[k]
310
+
311
+ total = sum(dict_importance.values())
312
+ return np.array([v / total for _, v in dict_importance.items()])
313
+
314
+ def _pool_parti(self, emb: torch.Tensor, attentions: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
315
+ maxed_attentions = self._create_pooled_matrices_across_layers(attentions).numpy()
316
+ emb_pooled = []
317
+ for e, a, mask in zip(emb, maxed_attentions, attention_mask):
318
+ dict_importance = self._page_rank(a)
319
+ importance_weights = self._calculate_importance_weights(dict_importance, mask)
320
+ num_tokens = int(mask.sum().item())
321
+ emb_pooled.append(np.average(e[:num_tokens], weights=importance_weights, axis=0))
322
+ pooled = torch.tensor(np.array(emb_pooled))
323
+ return pooled
324
+
325
+ def mean_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
326
+ if attention_mask is None:
327
+ return emb.mean(dim=1)
328
+ else:
329
+ attention_mask = attention_mask.unsqueeze(-1)
330
+ return (emb * attention_mask).sum(dim=1) / attention_mask.sum(dim=1)
331
+
332
+ def max_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
333
+ if attention_mask is None:
334
+ return emb.max(dim=1).values
335
+ else:
336
+ mask = attention_mask.unsqueeze(-1).bool()
337
+ return emb.masked_fill(~mask, float('-inf')).max(dim=1).values
338
+
339
+ def norm_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
340
+ if attention_mask is None:
341
+ return emb.norm(dim=1, p=2)
342
+ else:
343
+ attention_mask = attention_mask.unsqueeze(-1)
344
+ return (emb * attention_mask).norm(dim=1, p=2)
345
+
346
+ def median_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
347
+ if attention_mask is None:
348
+ return emb.median(dim=1).values
349
+ else:
350
+ mask = attention_mask.unsqueeze(-1).bool()
351
+ return emb.masked_fill(~mask, float('nan')).nanmedian(dim=1).values
352
+
353
+ def std_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
354
+ if attention_mask is None:
355
+ return emb.std(dim=1)
356
+ else:
357
+ var = self.var_pooling(emb, attention_mask, **kwargs)
358
+ return torch.sqrt(var)
359
+
360
+ def var_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
361
+ if attention_mask is None:
362
+ return emb.var(dim=1)
363
+ else:
364
+ attention_mask = attention_mask.unsqueeze(-1)
365
+ mean = (emb * attention_mask).sum(dim=1) / attention_mask.sum(dim=1)
366
+ mean = mean.unsqueeze(1)
367
+ squared_diff = (emb - mean) ** 2
368
+ var = (squared_diff * attention_mask).sum(dim=1) / attention_mask.sum(dim=1)
369
+ return var
370
+
371
+ def cls_pooling(self, emb: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, **kwargs) -> torch.Tensor:
372
+ return emb[:, 0, :]
373
+
374
+ def __call__(
375
+ self,
376
+ emb: torch.Tensor,
377
+ attention_mask: Optional[torch.Tensor] = None,
378
+ attentions: Optional[torch.Tensor] = None
379
+ ) -> torch.Tensor:
380
+ if attention_mask is not None:
381
+ assert attention_mask.sum(dim=-1).min() > 0, (
382
+ "Pooler received samples with all-zero attention masks. "
383
+ "This causes NaN from division by zero. Filter empty inputs before pooling."
384
+ )
385
+ final_emb: List[torch.Tensor] = []
386
+ for pooling_type in self.pooling_types:
387
+ final_emb.append(self.pooling_options[pooling_type](emb=emb, attention_mask=attention_mask, attentions=attentions))
388
+ return torch.cat(final_emb, dim=-1)
389
+
390
+
391
+ class ProteinDataset(TorchDataset):
392
+ """Simple dataset for protein sequences."""
393
+ def __init__(self, sequences: List[str]) -> None:
394
+ self.sequences = sequences
395
+
396
+ def __len__(self) -> int:
397
+ return len(self.sequences)
398
+
399
+ def __getitem__(self, idx: int) -> str:
400
+ return self.sequences[idx]
401
+
402
+
403
+ def parse_fasta(fasta_path: str) -> List[str]:
404
+ assert os.path.exists(fasta_path), f"FASTA file does not exist: {fasta_path}"
405
+ sequences = []
406
+ current_seq = []
407
+ with open(fasta_path, 'r') as f:
408
+ for line in f:
409
+ line = line.strip()
410
+ if not line:
411
+ continue
412
+ if line.startswith('>'):
413
+ if current_seq:
414
+ sequences.append(''.join(current_seq))
415
+ current_seq = []
416
+ else:
417
+ current_seq.append(line)
418
+ if current_seq:
419
+ sequences.append(''.join(current_seq))
420
+ return sequences
421
+
422
+
423
+ class EmbeddingMixin:
424
+ def _embed(self, input_ids: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
425
+ raise NotImplementedError
426
+
427
+ @property
428
+ def device(self) -> torch.device:
429
+ """Get the device of the model."""
430
+ return next(self.parameters()).device
431
+
432
+ def _read_sequences_from_db(self, db_path: str) -> Set[str]:
433
+ """Read sequences from SQLite database."""
434
+ with sqlite3.connect(db_path, timeout=30) as conn:
435
+ c = conn.cursor()
436
+ c.execute("SELECT sequence FROM embeddings")
437
+ return {row[0] for row in c.fetchall()}
438
+
439
+ def _ensure_embeddings_table(self, conn: sqlite3.Connection) -> None:
440
+ cursor = conn.cursor()
441
+ cursor.execute(
442
+ "CREATE TABLE IF NOT EXISTS embeddings ("
443
+ "sequence TEXT PRIMARY KEY, "
444
+ "embedding BLOB NOT NULL"
445
+ ")"
446
+ )
447
+ conn.commit()
448
+
449
+ def load_embeddings_from_pth(self, save_path: str) -> Dict[str, torch.Tensor]:
450
+ assert os.path.exists(save_path), f"Embedding file does not exist: {save_path}"
451
+ payload = torch.load(save_path, map_location="cpu", weights_only=True)
452
+ assert isinstance(payload, dict), "Expected .pth embeddings file to contain a dictionary."
453
+ for sequence, tensor in payload.items():
454
+ assert isinstance(sequence, str), "Expected embedding dictionary keys to be sequences (str)."
455
+ assert isinstance(tensor, torch.Tensor), "Expected embedding dictionary values to be tensors."
456
+ return payload
457
+
458
+ def load_embeddings_from_db(self, db_path: str, sequences: Optional[List[str]] = None) -> Dict[str, torch.Tensor]:
459
+ assert os.path.exists(db_path), f"Embedding database does not exist: {db_path}"
460
+ loaded: Dict[str, torch.Tensor] = {}
461
+ with sqlite3.connect(db_path, timeout=30) as conn:
462
+ self._ensure_embeddings_table(conn)
463
+ cursor = conn.cursor()
464
+ if sequences is None:
465
+ cursor.execute("SELECT sequence, embedding FROM embeddings")
466
+ else:
467
+ if len(sequences) == 0:
468
+ return loaded
469
+ placeholders = ",".join(["?"] * len(sequences))
470
+ cursor.execute(
471
+ f"SELECT sequence, embedding FROM embeddings WHERE sequence IN ({placeholders})",
472
+ tuple(sequences),
473
+ )
474
+
475
+ rows = cursor.fetchall()
476
+ for row in rows:
477
+ sequence = row[0]
478
+ embedding_bytes = row[1]
479
+ loaded[sequence] = embedding_blob_to_tensor(embedding_bytes)
480
+ return loaded
481
+
482
+ def embed_dataset(
483
+ self,
484
+ sequences: Optional[List[str]] = None,
485
+ tokenizer: Optional[PreTrainedTokenizerBase] = None,
486
+ batch_size: int = 2,
487
+ max_len: int = 512,
488
+ truncate: bool = True,
489
+ full_embeddings: bool = False,
490
+ embed_dtype: torch.dtype = torch.float32,
491
+ pooling_types: List[str] = ['mean'],
492
+ num_workers: int = 0,
493
+ sql: bool = False,
494
+ save: bool = True,
495
+ sql_db_path: str = 'embeddings.db',
496
+ save_path: str = 'embeddings.pth',
497
+ fasta_path: Optional[str] = None,
498
+ padding: str = 'max_length',
499
+ **kwargs,
500
+ ) -> Optional[Dict[str, torch.Tensor]]:
501
+ """
502
+ Embed a dataset of protein sequences.
503
+
504
+ Supports two modes:
505
+ - Tokenizer mode (ESM2/ESM++): provide `tokenizer` or use `self.tokenizer`.
506
+ - Sequence mode (E1): pass `tokenizer=None`, `_embed(sequences, return_attention_mask=True, **kwargs)` is used.
507
+
508
+ Sequences can be supplied as a list via `sequences`, parsed from a FASTA file via
509
+ `fasta_path`, or both (the two sources are combined). At least one must be provided.
510
+ """
511
+ if fasta_path is not None:
512
+ fasta_sequences = parse_fasta(fasta_path)
513
+ sequences = list(sequences or []) + fasta_sequences
514
+ assert sequences is not None and len(sequences) > 0, \
515
+ "Must provide at least one sequence via `sequences` or `fasta_path`."
516
+ sequences = list(set([seq[:max_len] if truncate else seq for seq in sequences]))
517
+ sequences = sorted(sequences, key=len, reverse=True)
518
+ hidden_size = self.config.hidden_size
519
+ pooler = Pooler(pooling_types) if not full_embeddings else None
520
+ if tokenizer is None and self.config.model_type != "E1":
521
+ tokenizer = self.tokenizer
522
+ tokenizer_mode = tokenizer is not None
523
+
524
+ # Resolve padding and compilation
525
+ dynamic = padding == 'longest'
526
+ compiled_model = maybe_compile(self, dynamic=dynamic)
527
+
528
+ if tokenizer_mode:
529
+ collate_fn = build_collator(tokenizer, padding=padding, max_length=max_len)
530
+ device = self.device
531
+ else:
532
+ collate_fn = None
533
+ device = None
534
+
535
+ def get_embeddings(residue_embeddings: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
536
+ assert isinstance(residue_embeddings, torch.Tensor)
537
+ if full_embeddings or residue_embeddings.ndim == 2:
538
+ return residue_embeddings
539
+ return pooler(residue_embeddings, attention_mask)
540
+
541
+ def iter_batches(to_embed: List[str]):
542
+ if tokenizer_mode:
543
+ assert collate_fn is not None
544
+ assert device is not None
545
+ dataset = ProteinDataset(to_embed)
546
+ dataloader = DataLoader(
547
+ dataset,
548
+ batch_size=batch_size,
549
+ num_workers=num_workers,
550
+ prefetch_factor=2 if num_workers > 0 else None,
551
+ collate_fn=collate_fn,
552
+ shuffle=False,
553
+ pin_memory=True,
554
+ )
555
+ for i, batch in _make_embedding_progress(dataloader, padding):
556
+ seqs = to_embed[i * batch_size:(i + 1) * batch_size]
557
+ input_ids = batch['input_ids'].to(device)
558
+ attention_mask = batch['attention_mask'].to(device)
559
+ residue_embeddings = compiled_model._embed(input_ids, attention_mask)
560
+ yield seqs, residue_embeddings, attention_mask
561
+ else:
562
+ for batch_start in tqdm(range(0, len(to_embed), batch_size), desc='Embedding batches'):
563
+ seqs = to_embed[batch_start:batch_start + batch_size]
564
+ batch_output = compiled_model._embed(seqs, return_attention_mask=True, **kwargs)
565
+ assert isinstance(batch_output, tuple), "Sequence mode _embed must return (last_hidden_state, attention_mask)."
566
+ assert len(batch_output) == 2, "Sequence mode _embed must return exactly two values."
567
+ residue_embeddings, attention_mask = batch_output
568
+ assert isinstance(attention_mask, torch.Tensor), "Sequence mode _embed must return attention_mask as a torch.Tensor."
569
+ yield seqs, residue_embeddings, attention_mask
570
+
571
+ if sql:
572
+ # Step 1: DEDUPLICATE - check existing embeddings in SQL
573
+ conn = sqlite3.connect(sql_db_path, timeout=30, check_same_thread=False)
574
+ conn.execute('PRAGMA journal_mode=WAL')
575
+ conn.execute('PRAGMA busy_timeout=30000')
576
+ conn.execute('PRAGMA synchronous=OFF')
577
+ conn.execute('PRAGMA cache_size=-64000')
578
+ self._ensure_embeddings_table(conn)
579
+ already_embedded = self._read_sequences_from_db(sql_db_path)
580
+ to_embed = [seq for seq in sequences if seq not in already_embedded]
581
+ print(f"Found {len(already_embedded)} already embedded sequences in {sql_db_path}")
582
+ print(f"Embedding {len(to_embed)} new sequences")
583
+ if len(to_embed) > 0:
584
+ # Steps 4-7: BATCH+EMBED -> POOL/TRIM -> SERIALIZE -> WRITE (async)
585
+ with _SQLWriter(conn) as writer:
586
+ with torch.inference_mode():
587
+ for seqs, residue_embeddings, attention_mask in iter_batches(to_embed):
588
+ embeddings = get_embeddings(residue_embeddings, attention_mask).to(embed_dtype)
589
+ if full_embeddings:
590
+ batch_rows = []
591
+ for seq, emb, mask in zip(seqs, embeddings, attention_mask):
592
+ batch_rows.append((seq, tensor_to_embedding_blob(emb[mask.bool()].reshape(-1, hidden_size))))
593
+ else:
594
+ blobs = batch_tensor_to_blobs(embeddings)
595
+ batch_rows = list(zip(seqs, blobs))
596
+ writer.write_batch(batch_rows)
597
+ conn.close()
598
+ return None
599
+
600
+ embeddings_dict = {}
601
+ if os.path.exists(save_path):
602
+ embeddings_dict = self.load_embeddings_from_pth(save_path)
603
+ to_embed = [seq for seq in sequences if seq not in embeddings_dict]
604
+ print(f"Found {len(embeddings_dict)} already embedded sequences in {save_path}")
605
+ print(f"Embedding {len(to_embed)} new sequences")
606
+ else:
607
+ to_embed = sequences
608
+ print(f"Embedding {len(to_embed)} new sequences")
609
+
610
+ if len(to_embed) > 0:
611
+ with torch.inference_mode():
612
+ for seqs, residue_embeddings, attention_mask in iter_batches(to_embed):
613
+ embeddings = get_embeddings(residue_embeddings, attention_mask).to(embed_dtype)
614
+ for seq, emb, mask in zip(seqs, embeddings, attention_mask):
615
+ if full_embeddings:
616
+ emb = emb[mask.bool()].reshape(-1, hidden_size)
617
+ embeddings_dict[seq] = emb.cpu()
618
+
619
+ if save:
620
+ torch.save(embeddings_dict, save_path)
621
+
622
+ return embeddings_dict
623
+
624
+
625
+ if __name__ == "__main__":
626
+ # py -m pooler
627
+ pooler = Pooler(pooling_types=['max', 'parti'])
628
+ batch_size = 8
629
+ seq_len = 64
630
+ hidden_size = 128
631
+ num_layers = 12
632
+ emb = torch.randn(batch_size, seq_len, hidden_size)
633
+ attentions = torch.randn(batch_size, num_layers, seq_len, seq_len)
634
+ attention_mask = torch.ones(batch_size, seq_len)
635
+ y = pooler(emb=emb, attention_mask=attention_mask, attentions=attentions)
636
+ print(y.shape)
637
+
638
+ """Shared attention infrastructure for all FastPLMs models.
639
+
640
+ Contains: AttentionBackend enum, backend resolution, mask creation,
641
+ flex attention helpers, flash kernel detection/dispatch, and pad/unpad utilities.
642
+ """
643
+ from enum import Enum
644
+ from typing import Dict, List, Optional, Tuple
645
+
646
+ import torch
647
+ import torch.nn as nn
648
+ from torch.nn import functional as F
649
+ from einops import rearrange
650
+
651
+ try:
652
+ from torch.nn.attention.flex_attention import create_block_mask, flex_attention, BlockMask
653
+ except ImportError:
654
+ create_block_mask = None
655
+ flex_attention = None
656
+ BlockMask = None
657
+
658
+ _compiled_flex_attention = None
659
+
660
+
661
+ def _get_flex_attention_fn():
662
+ """Return flex_attention callable: compiled (fused kernel) by default, or eager when debug flag is set."""
663
+ global _compiled_flex_attention
664
+ if flex_attention is None:
665
+ return None
666
+ flex_mod = torch.nn.attention.flex_attention
667
+ if getattr(flex_mod, "_FLEX_ATTENTION_DISABLE_COMPILE_DEBUG", False):
668
+ return flex_attention
669
+ if _compiled_flex_attention is None:
670
+ _compiled_flex_attention = torch.compile(
671
+ flex_attention,
672
+ dynamic=False,
673
+ )
674
+ return _compiled_flex_attention
675
+
676
+
677
+ ### Kernels Flash Attention Detection
678
+ def _infer_kernels_flash_variant(kernel) -> Optional[str]:
679
+ if hasattr(kernel, "fwd") and hasattr(kernel, "varlen_fwd"):
680
+ return "flash_attn2"
681
+ if hasattr(kernel, "flash_attn_func") and hasattr(kernel, "flash_attn_varlen_func"):
682
+ return "flash_attn3"
683
+ return None
684
+
685
+
686
+ def _try_get_kernels_flash():
687
+ try:
688
+ from kernels import get_kernel
689
+ except ImportError:
690
+ return None, None
691
+
692
+ flash_kernel = None
693
+ flash_kernel_variant = None
694
+ try:
695
+ flash_kernel = get_kernel("kernels-community/flash-attn3")
696
+ flash_kernel_variant = _infer_kernels_flash_variant(flash_kernel)
697
+ assert flash_kernel_variant is not None, "Loaded flash-attn3 kernel does not expose a supported API."
698
+ except Exception:
699
+ try:
700
+ flash_kernel = get_kernel("kernels-community/flash-attn2")
701
+ flash_kernel_variant = _infer_kernels_flash_variant(flash_kernel)
702
+ assert flash_kernel_variant is not None, "Loaded flash-attn2 kernel does not expose a supported API."
703
+ except Exception:
704
+ flash_kernel = None
705
+ flash_kernel_variant = None
706
+ return flash_kernel, flash_kernel_variant
707
+
708
+
709
+ _FLASH_KERNELS_LOADED = False
710
+ FLASH_KERNEL = None
711
+ FLASH_KERNEL_VARIANT = None
712
+
713
+
714
+ def _ensure_flash_kernels_loaded():
715
+ global _FLASH_KERNELS_LOADED, FLASH_KERNEL, FLASH_KERNEL_VARIANT
716
+ if _FLASH_KERNELS_LOADED:
717
+ return
718
+ _FLASH_KERNELS_LOADED = True
719
+ FLASH_KERNEL, FLASH_KERNEL_VARIANT = _try_get_kernels_flash()
720
+
721
+
722
+ def _kernels_flash_forward(
723
+ query_states: torch.Tensor,
724
+ key_states: torch.Tensor,
725
+ value_states: torch.Tensor,
726
+ causal: bool = False,
727
+ softmax_scale: Optional[float] = None,
728
+ ) -> torch.Tensor:
729
+ """Flash-attention forward, optionally overriding the softmax scale.
730
+
731
+ When `softmax_scale is None`, the flash kernel applies its default
732
+ `1 / sqrt(head_dim)`. Pass `softmax_scale=1.0` if the caller has already
733
+ pre-scaled Q (the convention used by ESM2, DPLM, DPLM2, E1, ESMFold).
734
+ Failing to override when Q is pre-scaled produces DOUBLE scaling and
735
+ catastrophic downstream drift -- on DPLM-150M (30 layers) this was observed
736
+ as pooled-embedding cosine ~-0.12 and argmax agreement ~0.27 vs sdpa.
737
+ """
738
+ assert FLASH_KERNEL is not None, "Kernel Flash Attention is not available in this environment."
739
+ if FLASH_KERNEL_VARIANT == "flash_attn2":
740
+ return FLASH_KERNEL.fwd(
741
+ q=query_states, k=key_states, v=value_states,
742
+ softmax_scale=softmax_scale, is_causal=causal,
743
+ )[0]
744
+ if FLASH_KERNEL_VARIANT == "flash_attn3":
745
+ try:
746
+ output = FLASH_KERNEL.flash_attn_func(
747
+ q=query_states, k=key_states, v=value_states,
748
+ softmax_scale=softmax_scale, causal=causal,
749
+ )
750
+ except TypeError:
751
+ output = FLASH_KERNEL.flash_attn_func(
752
+ query_states, key_states, value_states,
753
+ 0.0, softmax_scale, causal,
754
+ )
755
+ if isinstance(output, tuple):
756
+ return output[0]
757
+ return output
758
+ raise AssertionError(f"Unsupported kernels flash attention variant: {FLASH_KERNEL_VARIANT}")
759
+
760
+
761
+ def _kernels_flash_varlen_forward(
762
+ query_states: torch.Tensor,
763
+ key_states: torch.Tensor,
764
+ value_states: torch.Tensor,
765
+ cu_seqlens_q: torch.Tensor,
766
+ cu_seqlens_k: torch.Tensor,
767
+ max_seqlen_in_batch_q: int,
768
+ max_seqlen_in_batch_k: int,
769
+ causal: bool = False,
770
+ softmax_scale: Optional[float] = None,
771
+ ) -> torch.Tensor:
772
+ """Varlen flash-attention forward, optionally overriding the softmax scale.
773
+
774
+ See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
775
+ passed when Q has been pre-scaled by the caller.
776
+ """
777
+ assert FLASH_KERNEL is not None, "Kernel Flash Attention is not available in this environment."
778
+ if FLASH_KERNEL_VARIANT == "flash_attn2":
779
+ return FLASH_KERNEL.varlen_fwd(
780
+ q=query_states, k=key_states, v=value_states,
781
+ cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k,
782
+ max_seqlen_q=max_seqlen_in_batch_q, max_seqlen_k=max_seqlen_in_batch_k,
783
+ softmax_scale=softmax_scale, is_causal=causal,
784
+ )[0]
785
+ if FLASH_KERNEL_VARIANT == "flash_attn3":
786
+ try:
787
+ output = FLASH_KERNEL.flash_attn_varlen_func(
788
+ q=query_states, k=key_states, v=value_states,
789
+ cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k,
790
+ max_seqlen_q=max_seqlen_in_batch_q, max_seqlen_k=max_seqlen_in_batch_k,
791
+ softmax_scale=softmax_scale, causal=causal,
792
+ )
793
+ except TypeError:
794
+ output = FLASH_KERNEL.flash_attn_varlen_func(
795
+ query_states, key_states, value_states,
796
+ cu_seqlens_q, cu_seqlens_k,
797
+ max_seqlen_in_batch_q, max_seqlen_in_batch_k,
798
+ 0.0, softmax_scale, causal,
799
+ )
800
+ if isinstance(output, tuple):
801
+ return output[0]
802
+ return output
803
+ raise AssertionError(f"Unsupported kernels flash attention variant: {FLASH_KERNEL_VARIANT}")
804
+
805
+
806
+ ### Unpad / Pad helpers for varlen flash attention
807
+ class IndexFirstAxis(torch.autograd.Function):
808
+ @staticmethod
809
+ def forward(ctx, input, indices) -> torch.Tensor:
810
+ ctx.save_for_backward(indices)
811
+ assert input.ndim >= 2
812
+ ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
813
+ second_dim = other_shape.numel()
814
+ return torch.gather(
815
+ rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
816
+ ).reshape(-1, *other_shape)
817
+
818
+ @staticmethod
819
+ def backward(ctx, grad_output) -> Tuple[torch.Tensor, None]:
820
+ (indices,) = ctx.saved_tensors
821
+ assert grad_output.ndim >= 2
822
+ other_shape = grad_output.shape[1:]
823
+ grad_output = rearrange(grad_output, "b ... -> b (...)")
824
+ grad_input = torch.zeros(
825
+ [ctx.first_axis_dim, grad_output.shape[1]], device=grad_output.device, dtype=grad_output.dtype
826
+ )
827
+ grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
828
+ return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
829
+
830
+
831
+ class IndexPutFirstAxis(torch.autograd.Function):
832
+ @staticmethod
833
+ def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
834
+ ctx.save_for_backward(indices)
835
+ assert indices.ndim == 1
836
+ assert values.ndim >= 2
837
+ output = torch.zeros(first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype)
838
+ output[indices] = values
839
+ return output
840
+
841
+ @staticmethod
842
+ def backward(ctx, grad_output) -> Tuple[torch.Tensor, None, None]:
843
+ (indices,) = ctx.saved_tensors
844
+ return grad_output[indices], None, None
845
+
846
+
847
+ index_first_axis = IndexFirstAxis.apply
848
+ index_put_first_axis = IndexPutFirstAxis.apply
849
+
850
+
851
+ def pad_input(hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int) -> torch.Tensor:
852
+ output = index_put_first_axis(hidden_states, indices, batch * seqlen)
853
+ return rearrange(output, "(b s) ... -> b s ...", b=batch)
854
+
855
+
856
+ def _unpad_input(
857
+ query_layer: torch.Tensor,
858
+ key_layer: torch.Tensor,
859
+ value_layer: torch.Tensor,
860
+ attention_mask_2d: torch.Tensor,
861
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, Tuple[torch.Tensor, torch.Tensor], Tuple[int, int]]:
862
+ batch_size, seq_len, num_heads, head_dim = query_layer.shape
863
+ seqlens = attention_mask_2d.sum(dim=1).int()
864
+ cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0))
865
+ max_seqlen = int(seqlens.max().item())
866
+ indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten()
867
+ query_layer = index_first_axis(query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices)
868
+ key_layer = index_first_axis(key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices)
869
+ value_layer = index_first_axis(value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices)
870
+ return query_layer, key_layer, value_layer, indices, (cu_seqlens, cu_seqlens), (max_seqlen, max_seqlen)
871
+
872
+
873
+ def kernels_flash_attention_func(
874
+ query_states: torch.Tensor,
875
+ key_states: torch.Tensor,
876
+ value_states: torch.Tensor,
877
+ attention_mask_2d: Optional[torch.Tensor] = None,
878
+ causal: bool = False,
879
+ softmax_scale: Optional[float] = None,
880
+ ) -> torch.Tensor:
881
+ """Public flash-attention entry point with optional padding handling.
882
+
883
+ `softmax_scale`:
884
+ None -> kernel applies its default `1 / sqrt(head_dim)`.
885
+ float -> kernel uses the given scale (pass 1.0 when Q is pre-scaled
886
+ by the caller).
887
+
888
+ IMPORTANT: if your family multiplies Q by `1/sqrt(head_dim)` before calling
889
+ this function (as ESM2, DPLM, DPLM2, E1, and ESMFold do) you MUST pass
890
+ `softmax_scale=1.0`. Otherwise the kernel applies its default scale ON TOP
891
+ of the caller's, producing effective scale `1/head_dim` and catastrophic
892
+ downstream drift that compounds across layers.
893
+ """
894
+ assert FLASH_KERNEL is not None, "Kernel Flash Attention is not available in this environment."
895
+ if not causal and attention_mask_2d is not None:
896
+ batch_size, q_len = query_states.shape[:2]
897
+ (
898
+ query_states, key_states, value_states,
899
+ indices_q, (cu_seqlens_q, cu_seqlens_k), (max_seqlen_q, max_seqlen_k),
900
+ ) = _unpad_input(query_states, key_states, value_states, attention_mask_2d)
901
+ attn_output_unpad = _kernels_flash_varlen_forward(
902
+ query_states=query_states, key_states=key_states, value_states=value_states,
903
+ cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k,
904
+ max_seqlen_in_batch_q=max_seqlen_q, max_seqlen_in_batch_k=max_seqlen_k,
905
+ softmax_scale=softmax_scale,
906
+ )
907
+ return pad_input(attn_output_unpad, indices_q, batch_size, q_len)
908
+ else:
909
+ return _kernels_flash_forward(
910
+ query_states=query_states, key_states=key_states, value_states=value_states,
911
+ causal=causal, softmax_scale=softmax_scale,
912
+ )
913
+
914
+
915
+ ### Attention Backend Enum & Resolution
916
+ class AttentionBackend(Enum):
917
+ AUTO = "auto"
918
+ KERNELS_FLASH = "kernels_flash"
919
+ FLEX = "flex"
920
+ SDPA = "sdpa"
921
+
922
+
923
+ VALID_ATTENTION_BACKENDS = tuple(b.value for b in AttentionBackend)
924
+
925
+
926
+ _BACKEND_CONFIRMED = False
927
+
928
+
929
+ def resolve_attention_backend(requested_backend: str) -> AttentionBackend:
930
+ global _BACKEND_CONFIRMED
931
+ assert requested_backend in VALID_ATTENTION_BACKENDS, (
932
+ f"Unsupported attention backend: {requested_backend}. Expected one of {VALID_ATTENTION_BACKENDS}."
933
+ )
934
+ if requested_backend in (AttentionBackend.AUTO.value, AttentionBackend.KERNELS_FLASH.value):
935
+ _ensure_flash_kernels_loaded()
936
+ if requested_backend == AttentionBackend.AUTO.value:
937
+ if FLASH_KERNEL is not None:
938
+ resolved = AttentionBackend.KERNELS_FLASH
939
+ elif flex_attention is not None:
940
+ resolved = AttentionBackend.FLEX
941
+ else:
942
+ resolved = AttentionBackend.SDPA
943
+ elif requested_backend == AttentionBackend.KERNELS_FLASH.value:
944
+ assert FLASH_KERNEL is not None, "Kernels Flash Attention is not available in this environment."
945
+ resolved = AttentionBackend.KERNELS_FLASH
946
+ elif requested_backend == AttentionBackend.FLEX.value:
947
+ assert flex_attention is not None, "Flex Attention is not available in this environment."
948
+ resolved = AttentionBackend.FLEX
949
+ elif requested_backend == AttentionBackend.SDPA.value:
950
+ resolved = AttentionBackend.SDPA
951
+ else:
952
+ raise AssertionError(f"Unsupported attention backend: {requested_backend}")
953
+ if not _BACKEND_CONFIRMED:
954
+ print(f"Attention backend: config='{requested_backend}' -> resolved='{resolved.value}'")
955
+ _BACKEND_CONFIRMED = True
956
+ return resolved
957
+
958
+
959
+ @torch.compiler.disable
960
+ def get_attention_mask(
961
+ effective_backend: AttentionBackend,
962
+ batch_size: int,
963
+ seq_len: int,
964
+ device: torch.device,
965
+ attention_mask: Optional[torch.Tensor] = None,
966
+ ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[BlockMask]]:
967
+ """Build padding masks once for all encoder layers.
968
+
969
+ Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
970
+ """
971
+ if attention_mask is None:
972
+ return None, None, None
973
+
974
+ attention_mask_2d = attention_mask.bool()
975
+
976
+ if effective_backend == AttentionBackend.KERNELS_FLASH:
977
+ return attention_mask_2d, None, None
978
+
979
+ if effective_backend == AttentionBackend.FLEX:
980
+ assert create_block_mask is not None, "Flex attention backend requested but torch.create_block_mask is unavailable."
981
+ valid_lens = attention_mask_2d.sum(dim=-1)
982
+
983
+ def mask_mod(batch_idx, head_idx, q_idx, kv_idx):
984
+ return (q_idx < valid_lens[batch_idx]) & (kv_idx < valid_lens[batch_idx])
985
+
986
+ flex_block_mask = create_block_mask(mask_mod, batch_size, 1, seq_len, seq_len, device=device)
987
+ return attention_mask_2d, None, flex_block_mask
988
+
989
+ # SDPA / manual -- only mask the key dimension so padding query positions attend to
990
+ # real keys and produce valid (non-NaN) outputs instead of NaN from softmax(-inf,...,-inf).
991
+ attention_mask_4d = attention_mask_2d[:, None, None, :]
992
+ return attention_mask_2d, attention_mask_4d, None
993
+
994
+
995
+ def bool_to_additive_mask(
996
+ bool_mask: torch.Tensor,
997
+ dtype: torch.dtype,
998
+ ) -> torch.Tensor:
999
+ """Convert a bool mask (True = valid) to a float additive mask (0.0 valid, -inf invalid).
1000
+
1001
+ Why this exists: calling `bool_mask.masked_fill(bool_mask.logical_not(), float('-inf'))`
1002
+ directly on a bool tensor returns a bool tensor -- because `-inf` casts to `True` -- and
1003
+ silently drops the mask entirely. Always allocate a float tensor first, then fill it.
1004
+ This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
1005
+ """
1006
+ assert bool_mask.dtype == torch.bool, (
1007
+ f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
1008
+ )
1009
+ additive = torch.zeros_like(bool_mask, dtype=dtype)
1010
+ additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
1011
+ return additive
1012
+
1013
  """
1014
  ESM++ model implementation.
1015
 
 
1039
  from transformers import PreTrainedModel, PreTrainedTokenizerFast, PretrainedConfig
1040
  from transformers.modeling_outputs import ModelOutput
1041
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1042
 
1043
 
1044
  class ESMplusplusConfig(PretrainedConfig):