pepe-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pepe/__init__.py +41 -0
- pepe/__main__.py +23 -0
- pepe/embedders/__init__.py +0 -0
- pepe/embedders/base_embedder.py +797 -0
- pepe/embedders/custom_embedder.py +647 -0
- pepe/embedders/esm_embedder.py +164 -0
- pepe/embedders/huggingface_embedder.py +200 -0
- pepe/model_selecter.py +99 -0
- pepe/parse_arguments.py +167 -0
- pepe/utils.py +905 -0
- pepe_cli-1.0.0.dist-info/METADATA +137 -0
- pepe_cli-1.0.0.dist-info/RECORD +18 -0
- pepe_cli-1.0.0.dist-info/WHEEL +5 -0
- pepe_cli-1.0.0.dist-info/entry_points.txt +3 -0
- pepe_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- pepe_cli-1.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_run.py +43 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv
|
|
3
|
+
import torch
|
|
4
|
+
import re
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.lib.format import open_memmap
|
|
7
|
+
import inspect
|
|
8
|
+
import gc
|
|
9
|
+
from pepe.utils import MultiIODispatcher, check_disk_free_space
|
|
10
|
+
from alive_progress import alive_bar
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("src.embedders.base_embedder")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseEmbedder:
|
|
19
|
+
def __init__(self, args):
|
|
20
|
+
self.fasta_path = args.fasta_path
|
|
21
|
+
self.model_link = args.model_name
|
|
22
|
+
self.disable_special_tokens = args.disable_special_tokens
|
|
23
|
+
if (
|
|
24
|
+
self.model_link.endswith(".pt")
|
|
25
|
+
or self.model_link.endswith(".pth")
|
|
26
|
+
or self.model_link.startswith("custom:")
|
|
27
|
+
or (
|
|
28
|
+
os.path.exists(self.model_link)
|
|
29
|
+
and (os.path.isfile(self.model_link) or os.path.isdir(self.model_link))
|
|
30
|
+
)
|
|
31
|
+
):
|
|
32
|
+
self.model_name = Path(
|
|
33
|
+
self.model_link
|
|
34
|
+
).stem # Use the stem of the model link as the model name
|
|
35
|
+
else:
|
|
36
|
+
self.model_name = re.sub(r"^.*?/", "", self.model_link)
|
|
37
|
+
self.output_path = os.path.join(args.output_path, self.model_name)
|
|
38
|
+
# Check if output directory exists and creates it if it's missing
|
|
39
|
+
if not os.path.exists(self.output_path):
|
|
40
|
+
os.makedirs(self.output_path)
|
|
41
|
+
if not args.experiment_name:
|
|
42
|
+
self.output_prefix = os.path.splitext(os.path.basename(self.fasta_path))[
|
|
43
|
+
0
|
|
44
|
+
] # get filename without extension and path
|
|
45
|
+
else:
|
|
46
|
+
self.output_prefix = args.experiment_name
|
|
47
|
+
self.substring_path = args.substring_path
|
|
48
|
+
self.context = args.context
|
|
49
|
+
self.layers = (
|
|
50
|
+
[j for i in args.layers for j in i] if args.layers != [None] else None
|
|
51
|
+
)
|
|
52
|
+
self.substring_dict = (
|
|
53
|
+
self._load_substrings(args.substring_path) if args.substring_path else None
|
|
54
|
+
)
|
|
55
|
+
self.batch_size = args.batch_size
|
|
56
|
+
self.max_length = args.max_length
|
|
57
|
+
if torch.cuda.is_available() and args.device == "cuda":
|
|
58
|
+
self.device = torch.device("cuda")
|
|
59
|
+
else:
|
|
60
|
+
self.device = torch.device("cpu")
|
|
61
|
+
self.output_types = args.extract_embeddings
|
|
62
|
+
self.discard_padding = args.discard_padding
|
|
63
|
+
self.flatten = args.flatten
|
|
64
|
+
self.return_embeddings = False
|
|
65
|
+
self.return_contacts = False
|
|
66
|
+
self.return_logits = False
|
|
67
|
+
for output_type in self.output_types:
|
|
68
|
+
if "pooled" in output_type or "per_token" in output_type:
|
|
69
|
+
self.return_embeddings = True
|
|
70
|
+
if "attention" in output_type:
|
|
71
|
+
self.return_contacts = True
|
|
72
|
+
if output_type == "logits":
|
|
73
|
+
self.return_logits = True
|
|
74
|
+
self.streaming_output = args.streaming_output
|
|
75
|
+
self.num_workers = args.num_workers if self.streaming_output else 1
|
|
76
|
+
self.max_in_flight = self.num_workers * 2
|
|
77
|
+
self.flush_batches_after = args.flush_batches_after * 1024**2 # in bytes
|
|
78
|
+
self.precision = args.precision
|
|
79
|
+
# self.log_memory = args.log_memory # TODO implement memory logging
|
|
80
|
+
|
|
81
|
+
# Set up checkpoint directory for crash recovery
|
|
82
|
+
self.checkpoint_dir = self.output_path
|
|
83
|
+
|
|
84
|
+
def _precision_to_dtype(self, precision, framework):
|
|
85
|
+
half_precision = ["float16", "16", "half"]
|
|
86
|
+
full_precision = ["float32", "32", "full"]
|
|
87
|
+
if precision in half_precision:
|
|
88
|
+
if framework == "torch":
|
|
89
|
+
return torch.float16
|
|
90
|
+
elif framework == "numpy":
|
|
91
|
+
return np.float16
|
|
92
|
+
elif precision in full_precision:
|
|
93
|
+
if framework == "torch":
|
|
94
|
+
return torch.float32
|
|
95
|
+
elif framework == "numpy":
|
|
96
|
+
return np.float32
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Unsupported precision: {precision}. Supported values are {half_precision} or {full_precision}."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _set_output_objects(self):
|
|
103
|
+
"""Initialize output objects."""
|
|
104
|
+
self.sequence_labels = []
|
|
105
|
+
self.logits = {
|
|
106
|
+
"output_data": {layer: [] for layer in self.layers}, # type: ignore
|
|
107
|
+
"method": self._extract_logits,
|
|
108
|
+
"output_dir": os.path.join(self.output_path, "logits"),
|
|
109
|
+
"shape": (
|
|
110
|
+
self.num_sequences, # type: ignore
|
|
111
|
+
self.max_length,
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
self.mean_pooled = {
|
|
115
|
+
"output_data": {layer: [] for layer in self.layers}, # type: ignore
|
|
116
|
+
"method": self._extract_mean_pooled,
|
|
117
|
+
"output_dir": os.path.join(self.output_path, "mean_pooled"),
|
|
118
|
+
"shape": (self.num_sequences, self.embedding_size), # type: ignore
|
|
119
|
+
}
|
|
120
|
+
self.per_token = {
|
|
121
|
+
"output_data": {layer: [] for layer in self.layers}, # type: ignore
|
|
122
|
+
"method": self._extract_per_token,
|
|
123
|
+
"output_dir": os.path.join(self.output_path, "per_token"),
|
|
124
|
+
"shape": (
|
|
125
|
+
(
|
|
126
|
+
self.num_sequences, # type: ignore
|
|
127
|
+
self.max_length,
|
|
128
|
+
self.embedding_size, # type: ignore
|
|
129
|
+
)
|
|
130
|
+
if not self.flatten
|
|
131
|
+
else (
|
|
132
|
+
self.num_sequences, # type: ignore
|
|
133
|
+
self.max_length * self.embedding_size, # type: ignore
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
self.substring_pooled = {
|
|
138
|
+
"output_data": {layer: [] for layer in self.layers}, # type: ignore
|
|
139
|
+
"method": self._extract_substring_pooled,
|
|
140
|
+
"output_dir": os.path.join(self.output_path, "substring_pooled"),
|
|
141
|
+
"shape": (self.num_sequences, self.embedding_size), # type: ignore
|
|
142
|
+
}
|
|
143
|
+
self.attention_head = {
|
|
144
|
+
"output_data": {
|
|
145
|
+
layer: {head: [] for head in range(self.num_heads)} # type: ignore
|
|
146
|
+
for layer in self.layers # type: ignore
|
|
147
|
+
},
|
|
148
|
+
"method": self._extract_attention_head,
|
|
149
|
+
"output_dir": os.path.join(self.output_path, "attention_head"),
|
|
150
|
+
"shape": (
|
|
151
|
+
(
|
|
152
|
+
self.num_sequences, # type: ignore
|
|
153
|
+
self.max_length,
|
|
154
|
+
self.max_length,
|
|
155
|
+
)
|
|
156
|
+
if not self.flatten
|
|
157
|
+
else (
|
|
158
|
+
self.num_sequences, # type: ignore
|
|
159
|
+
self.max_length**2,
|
|
160
|
+
)
|
|
161
|
+
),
|
|
162
|
+
}
|
|
163
|
+
self.attention_layer = {
|
|
164
|
+
"output_data": {layer: [] for layer in self.layers}, # type: ignore
|
|
165
|
+
"method": self._extract_attention_layer,
|
|
166
|
+
"output_dir": os.path.join(self.output_path, "attention_layer"),
|
|
167
|
+
"shape": (
|
|
168
|
+
(
|
|
169
|
+
self.num_sequences, # type: ignore
|
|
170
|
+
self.max_length,
|
|
171
|
+
self.max_length,
|
|
172
|
+
)
|
|
173
|
+
if not self.flatten
|
|
174
|
+
else (
|
|
175
|
+
self.num_sequences, # type: ignore
|
|
176
|
+
self.max_length**2,
|
|
177
|
+
)
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
self.attention_model = {
|
|
181
|
+
"output_data": [],
|
|
182
|
+
"method": self._extract_attention_model,
|
|
183
|
+
"output_dir": os.path.join(self.output_path, "attention_model"),
|
|
184
|
+
"shape": (
|
|
185
|
+
(
|
|
186
|
+
self.num_sequences, # type: ignore
|
|
187
|
+
self.max_length,
|
|
188
|
+
self.max_length,
|
|
189
|
+
)
|
|
190
|
+
if not self.flatten
|
|
191
|
+
else (
|
|
192
|
+
self.num_sequences, # type: ignore
|
|
193
|
+
self.max_length**2,
|
|
194
|
+
)
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# When changes made here, also update base_embedder.py BaseEmbedder.set_output_objects() method.
|
|
199
|
+
def _get_output_types(self, args):
|
|
200
|
+
output_types = []
|
|
201
|
+
|
|
202
|
+
options_mapping = {
|
|
203
|
+
"per_token": "per_token",
|
|
204
|
+
"mean_pooled": "mean_pooled",
|
|
205
|
+
"substring_pooled": "substring_pooled",
|
|
206
|
+
"attention_head": "attention_head",
|
|
207
|
+
"attention_layer": "attention_layer",
|
|
208
|
+
"attention_model": "attention_model",
|
|
209
|
+
"logits": "logits",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for option in args.extract_embeddings:
|
|
213
|
+
if option in options_mapping:
|
|
214
|
+
output_type = options_mapping[option]
|
|
215
|
+
if output_type not in output_types:
|
|
216
|
+
output_types.append(output_type)
|
|
217
|
+
|
|
218
|
+
return output_types
|
|
219
|
+
|
|
220
|
+
def _make_output_filepath(self, output_type, output_dir, layer=None, head=None):
|
|
221
|
+
base = f"{self.output_prefix}_{self.model_name}_{output_type}"
|
|
222
|
+
if layer is not None:
|
|
223
|
+
base += f"_layer_{layer}"
|
|
224
|
+
if head is not None:
|
|
225
|
+
base += f"_head_{head + 1}"
|
|
226
|
+
return os.path.join(output_dir, base + ".npy")
|
|
227
|
+
|
|
228
|
+
def preallocate_disk_space(self):
|
|
229
|
+
memmap_registry = {}
|
|
230
|
+
total_bytes = 0
|
|
231
|
+
for output_type in self.output_types:
|
|
232
|
+
output_data = getattr(self, output_type)["output_data"]
|
|
233
|
+
shape = getattr(self, output_type)["shape"]
|
|
234
|
+
output_dir = getattr(self, output_type)["output_dir"]
|
|
235
|
+
np_dtype = self._precision_to_dtype(self.precision, "numpy")
|
|
236
|
+
bytes_per_array = np.dtype(np_dtype).itemsize * np.prod(shape) # type: ignore
|
|
237
|
+
|
|
238
|
+
if isinstance(output_data, dict):
|
|
239
|
+
for layer in self.layers: # type: ignore
|
|
240
|
+
if isinstance(output_data[layer], dict): # e.g., all_heads
|
|
241
|
+
for head in range(self.num_heads): # type: ignore
|
|
242
|
+
file_path = self._make_output_filepath(
|
|
243
|
+
output_type, output_dir, layer, head
|
|
244
|
+
)
|
|
245
|
+
mode = "r+" if os.path.exists(file_path) else "w+"
|
|
246
|
+
output_data[layer][head] = open_memmap(
|
|
247
|
+
file_path, mode=mode, dtype=np_dtype, shape=shape
|
|
248
|
+
)
|
|
249
|
+
memmap_registry[(output_type, layer, head)] = output_data[
|
|
250
|
+
layer
|
|
251
|
+
][head]
|
|
252
|
+
total_bytes += bytes_per_array
|
|
253
|
+
else:
|
|
254
|
+
file_path = self._make_output_filepath(
|
|
255
|
+
output_type, output_dir, layer
|
|
256
|
+
)
|
|
257
|
+
mode = "r+" if os.path.exists(file_path) else "w+"
|
|
258
|
+
output_data[layer] = open_memmap(
|
|
259
|
+
file_path, mode=mode, dtype=np_dtype, shape=shape
|
|
260
|
+
)
|
|
261
|
+
memmap_registry[(output_type, layer, None)] = output_data[layer]
|
|
262
|
+
total_bytes += bytes_per_array
|
|
263
|
+
else:
|
|
264
|
+
file_path = self._make_output_filepath(output_type, output_dir)
|
|
265
|
+
mode = "r+" if os.path.exists(file_path) else "w+"
|
|
266
|
+
output_array = open_memmap(
|
|
267
|
+
file_path, mode=mode, dtype=np_dtype, shape=shape
|
|
268
|
+
)
|
|
269
|
+
setattr(getattr(self, output_type), "output_data", output_array)
|
|
270
|
+
memmap_registry[(output_type, None, None)] = output_array
|
|
271
|
+
total_bytes += bytes_per_array
|
|
272
|
+
|
|
273
|
+
logger.info(f"Preparing to write {total_bytes / 1024**3:.2f} GB to disk.")
|
|
274
|
+
check_disk_free_space(self.output_path, total_bytes)
|
|
275
|
+
return memmap_registry
|
|
276
|
+
|
|
277
|
+
def _load_substrings(self, substring_path):
|
|
278
|
+
"""Load substrings and store in a dictionary."""
|
|
279
|
+
if substring_path:
|
|
280
|
+
with open(substring_path) as f:
|
|
281
|
+
reader = csv.reader(f) # skip header
|
|
282
|
+
next(reader)
|
|
283
|
+
substring_dict = {rows[0]: rows[1] for rows in reader}
|
|
284
|
+
return substring_dict
|
|
285
|
+
else:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def _safe_compute(self, toks, attention_mask):
|
|
289
|
+
"""
|
|
290
|
+
Try to run compute_outputs; on OOM, empty cache, split in half,
|
|
291
|
+
recurse on each half, then concatenate.
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
return self._compute_outputs( # type: ignore
|
|
295
|
+
self.model, # type: ignore
|
|
296
|
+
toks,
|
|
297
|
+
attention_mask,
|
|
298
|
+
self.return_embeddings,
|
|
299
|
+
self.return_contacts,
|
|
300
|
+
self.return_logits,
|
|
301
|
+
)
|
|
302
|
+
except torch.OutOfMemoryError:
|
|
303
|
+
logger.error("[GPU memory overflow] Decreasing batch size and retrying...")
|
|
304
|
+
torch.cuda.empty_cache()
|
|
305
|
+
B = toks.size(0)
|
|
306
|
+
if B == 1:
|
|
307
|
+
# can’t split anymore
|
|
308
|
+
logger.error("OOM on single sample!")
|
|
309
|
+
raise
|
|
310
|
+
# split into two roughly equal chunks
|
|
311
|
+
half = B // 2
|
|
312
|
+
toks_chunks = torch.split(toks, [half, B - half], dim=0)
|
|
313
|
+
if attention_mask is not None:
|
|
314
|
+
mask_chunks = torch.split(attention_mask, [half, B - half], dim=0)
|
|
315
|
+
else:
|
|
316
|
+
mask_chunks = [None, None]
|
|
317
|
+
|
|
318
|
+
outs = [
|
|
319
|
+
self._safe_compute(tc, mc) for tc, mc in zip(toks_chunks, mask_chunks)
|
|
320
|
+
]
|
|
321
|
+
# outs is list of (logits, reps, attn)
|
|
322
|
+
logits = (
|
|
323
|
+
torch.cat([o[0] for o in outs], dim=0) if self.return_logits else None # type: ignore
|
|
324
|
+
)
|
|
325
|
+
representations = (
|
|
326
|
+
torch.cat([o[1] for o in outs], dim=0) # type: ignore
|
|
327
|
+
if self.return_embeddings
|
|
328
|
+
else None
|
|
329
|
+
)
|
|
330
|
+
attention_matrices = (
|
|
331
|
+
torch.cat([o[2] for o in outs], dim=0) if self.return_contacts else None # type: ignore
|
|
332
|
+
)
|
|
333
|
+
return logits, representations, attention_matrices
|
|
334
|
+
|
|
335
|
+
def embed(self):
|
|
336
|
+
if self.streaming_output:
|
|
337
|
+
# Start centralized I/O dispatcher with checkpoint support
|
|
338
|
+
self.io_dispatcher = MultiIODispatcher(
|
|
339
|
+
self.memmap_registry,
|
|
340
|
+
flush_bytes_limit=self.flush_batches_after,
|
|
341
|
+
heavy_output_type="per_token",
|
|
342
|
+
checkpoint_dir=self.checkpoint_dir,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Check if we're resuming from a checkpoint
|
|
346
|
+
resume_info = self.io_dispatcher.get_resume_info()
|
|
347
|
+
if resume_info:
|
|
348
|
+
logger.info(f"Resuming from checkpoint: {resume_info}")
|
|
349
|
+
|
|
350
|
+
with alive_bar(
|
|
351
|
+
len(self.sequences), # type: ignore
|
|
352
|
+
title=f"{self.model_name}: Generating embeddings ...",
|
|
353
|
+
) as bar, torch.no_grad():
|
|
354
|
+
offset = 0
|
|
355
|
+
for (
|
|
356
|
+
labels,
|
|
357
|
+
strs,
|
|
358
|
+
toks,
|
|
359
|
+
attention_mask,
|
|
360
|
+
substring_mask,
|
|
361
|
+
) in self.data_loader: # type: ignore
|
|
362
|
+
toks = toks.to(self.device, non_blocking=True)
|
|
363
|
+
if attention_mask is not None:
|
|
364
|
+
attention_mask = attention_mask.to(self.device, non_blocking=True)
|
|
365
|
+
pooling_mask = self._mask_special_tokens(
|
|
366
|
+
toks, self.special_tokens # type: ignore
|
|
367
|
+
).cpu() # mask special tokens to avoid diluting signal when pooling embeddings
|
|
368
|
+
logits, representations, attention_matrices = self._safe_compute(
|
|
369
|
+
toks, attention_mask
|
|
370
|
+
)
|
|
371
|
+
torch.cuda.empty_cache()
|
|
372
|
+
|
|
373
|
+
output_bundle = {
|
|
374
|
+
"logits": logits,
|
|
375
|
+
"attention_matrices": attention_matrices,
|
|
376
|
+
"representations": representations,
|
|
377
|
+
"batch_labels": labels,
|
|
378
|
+
"pooling_mask": pooling_mask,
|
|
379
|
+
"substring_mask": substring_mask,
|
|
380
|
+
"offset": offset,
|
|
381
|
+
"special_tokens": not self.disable_special_tokens,
|
|
382
|
+
}
|
|
383
|
+
if self.streaming_output:
|
|
384
|
+
# Apply backpressure if write queue is too full
|
|
385
|
+
while self.io_dispatcher.queue_fullness() > 0.6:
|
|
386
|
+
logger.warning(
|
|
387
|
+
"[embed] Backpressure: waiting for IOFlushWorker to catch up..."
|
|
388
|
+
)
|
|
389
|
+
time.sleep(0.05)
|
|
390
|
+
self._extract_batch(output_bundle)
|
|
391
|
+
|
|
392
|
+
del logits, representations, attention_matrices
|
|
393
|
+
gc.collect()
|
|
394
|
+
torch.cuda.empty_cache()
|
|
395
|
+
|
|
396
|
+
offset += len(toks)
|
|
397
|
+
|
|
398
|
+
self.sequence_labels.extend(labels)
|
|
399
|
+
bar(len(toks))
|
|
400
|
+
|
|
401
|
+
if self.streaming_output:
|
|
402
|
+
self.io_dispatcher.stop()
|
|
403
|
+
|
|
404
|
+
logger.info("Finished extracting embeddings")
|
|
405
|
+
|
|
406
|
+
# After successful completion, clean up the checkpoint file
|
|
407
|
+
if self.streaming_output:
|
|
408
|
+
self._cleanup_checkpoint()
|
|
409
|
+
|
|
410
|
+
def _cleanup_checkpoint(self):
|
|
411
|
+
"""Clean up the checkpoint file after successful completion."""
|
|
412
|
+
checkpoint_file = os.path.join(self.checkpoint_dir, "global_checkpoint.json")
|
|
413
|
+
if os.path.exists(checkpoint_file):
|
|
414
|
+
try:
|
|
415
|
+
os.remove(checkpoint_file)
|
|
416
|
+
logger.info(f"Cleaned up checkpoint file: {checkpoint_file}")
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.error(
|
|
419
|
+
f"Warning: Could not remove checkpoint file {checkpoint_file}: {e}"
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
logger.info("No checkpoint file found to clean up.")
|
|
423
|
+
|
|
424
|
+
def _load_data(self):
|
|
425
|
+
raise NotImplementedError(
|
|
426
|
+
"This method should be implemented in the child class"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _initialize_model(self):
|
|
430
|
+
raise NotImplementedError(
|
|
431
|
+
"This method should be implemented in the child class"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def _load_layers(self):
|
|
435
|
+
raise NotImplementedError(
|
|
436
|
+
"This method should be implemented in the child class"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def get_substring_positions(self, label, special_tokens, context=0):
|
|
440
|
+
"""Get the start and end positions of the substring in the full sequence."""
|
|
441
|
+
full_sequence = self.sequences[label] # type: ignore
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
substring = self.substring_dict[label] # type: ignore
|
|
445
|
+
except KeyError:
|
|
446
|
+
SystemExit(f"No matching substring found for {label}")
|
|
447
|
+
# remove '-' from substring
|
|
448
|
+
substring = substring.replace("-", "")
|
|
449
|
+
|
|
450
|
+
# get position of substring in sequence
|
|
451
|
+
start = max(full_sequence.find(substring) - context, 0) + int(
|
|
452
|
+
special_tokens
|
|
453
|
+
)
|
|
454
|
+
end = (
|
|
455
|
+
min(start + len(substring) + context, len(full_sequence))
|
|
456
|
+
+ special_tokens
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return start, end
|
|
460
|
+
|
|
461
|
+
def _extract_batch(
|
|
462
|
+
self,
|
|
463
|
+
output_bundle,
|
|
464
|
+
):
|
|
465
|
+
for output_type in self.output_types:
|
|
466
|
+
sig = inspect.signature(getattr(self, output_type)["method"])
|
|
467
|
+
needed_args = {
|
|
468
|
+
k: v for k, v in output_bundle.items() if k in sig.parameters
|
|
469
|
+
}
|
|
470
|
+
getattr(self, output_type)["method"](**needed_args)
|
|
471
|
+
# clear the output bundle to free up memory
|
|
472
|
+
output_bundle.clear()
|
|
473
|
+
del output_bundle
|
|
474
|
+
torch.cuda.empty_cache()
|
|
475
|
+
|
|
476
|
+
def _mask_special_tokens(self, input_tensor, special_tokens=None):
|
|
477
|
+
"""
|
|
478
|
+
Create a boolean mask for special tokens in the input tensor.
|
|
479
|
+
|
|
480
|
+
"""
|
|
481
|
+
if (
|
|
482
|
+
special_tokens is not None
|
|
483
|
+
): # Create a boolean mask: True where the value is not in special_tokens.
|
|
484
|
+
mask = ~torch.isin(input_tensor, special_tokens)
|
|
485
|
+
else: # Create a boolean mask: True where the value is not 0, 1, or 2.
|
|
486
|
+
mask = (input_tensor != 0) & (input_tensor != 1) & (input_tensor != 2)
|
|
487
|
+
# Convert and return the boolean mask to boolean type.
|
|
488
|
+
return mask
|
|
489
|
+
|
|
490
|
+
def _extract_logits(
|
|
491
|
+
self,
|
|
492
|
+
logits,
|
|
493
|
+
offset,
|
|
494
|
+
):
|
|
495
|
+
for layer in self.layers: # type: ignore
|
|
496
|
+
tensor = logits[layer - 1]
|
|
497
|
+
if self.streaming_output:
|
|
498
|
+
# output_file = self.logits["output_data"][layer]
|
|
499
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
500
|
+
|
|
501
|
+
self.io_dispatcher.enqueue(
|
|
502
|
+
output_type="logits",
|
|
503
|
+
layer=layer,
|
|
504
|
+
head=None,
|
|
505
|
+
offset=offset,
|
|
506
|
+
array=self._to_numpy(tensor), # Ensure it's on CPU and NumPy
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
self.logits["output_data"][layer].extend(tensor)
|
|
510
|
+
|
|
511
|
+
def _extract_mean_pooled(
|
|
512
|
+
self,
|
|
513
|
+
representations,
|
|
514
|
+
batch_labels,
|
|
515
|
+
pooling_mask,
|
|
516
|
+
offset,
|
|
517
|
+
):
|
|
518
|
+
for layer in self.layers: # type: ignore
|
|
519
|
+
tensor = torch.stack(
|
|
520
|
+
[
|
|
521
|
+
(
|
|
522
|
+
(pooling_mask[i].unsqueeze(-1) * representations[layer][i]).sum(
|
|
523
|
+
0
|
|
524
|
+
)
|
|
525
|
+
/ pooling_mask[i].unsqueeze(-1).sum(0)
|
|
526
|
+
)
|
|
527
|
+
for i in range(len(batch_labels))
|
|
528
|
+
]
|
|
529
|
+
)
|
|
530
|
+
if self.streaming_output:
|
|
531
|
+
# output_file = self.embeddings["output_data"][layer]
|
|
532
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
533
|
+
self.io_dispatcher.enqueue(
|
|
534
|
+
output_type="mean_pooled",
|
|
535
|
+
layer=layer,
|
|
536
|
+
head=None,
|
|
537
|
+
offset=offset,
|
|
538
|
+
array=self._to_numpy(tensor), # Ensure it's on CPU and NumPy
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
self.mean_pooled["output_data"][layer].extend(tensor)
|
|
542
|
+
|
|
543
|
+
def _extract_per_token(
|
|
544
|
+
self,
|
|
545
|
+
representations,
|
|
546
|
+
batch_labels,
|
|
547
|
+
offset,
|
|
548
|
+
):
|
|
549
|
+
if not self.discard_padding:
|
|
550
|
+
for layer in self.layers: # type: ignore
|
|
551
|
+
tensor = torch.stack(
|
|
552
|
+
[representations[layer][i] for i in range(len(batch_labels))]
|
|
553
|
+
)
|
|
554
|
+
if self.flatten:
|
|
555
|
+
tensor = tensor.flatten(start_dim=1)
|
|
556
|
+
if self.streaming_output:
|
|
557
|
+
# output_file = self.per_token["output_data"][layer]
|
|
558
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
559
|
+
self.io_dispatcher.enqueue(
|
|
560
|
+
output_type="per_token",
|
|
561
|
+
layer=layer,
|
|
562
|
+
head=None,
|
|
563
|
+
offset=offset,
|
|
564
|
+
array=np.ascontiguousarray(
|
|
565
|
+
tensor.cpu().numpy()
|
|
566
|
+
), # Ensure it's on CPU and NumPy
|
|
567
|
+
)
|
|
568
|
+
else:
|
|
569
|
+
self.per_token["output_data"][layer].extend(tensor)
|
|
570
|
+
else: # TODO remove padding tokens
|
|
571
|
+
logger.warning("Feature not implemented yet")
|
|
572
|
+
pass
|
|
573
|
+
for layer in self.layers: # type: ignore
|
|
574
|
+
if self.flatten:
|
|
575
|
+
self.per_token["output_data"][layer].extend(
|
|
576
|
+
[
|
|
577
|
+
representations[layer][i].flatten(start_dim=1)
|
|
578
|
+
for i in range(len(batch_labels))
|
|
579
|
+
]
|
|
580
|
+
)
|
|
581
|
+
else:
|
|
582
|
+
self.per_token["output_data"][layer].extend(
|
|
583
|
+
[representations[layer][i] for i in range(len(batch_labels))]
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def _extract_attention_head(
|
|
587
|
+
self,
|
|
588
|
+
attention_matrices,
|
|
589
|
+
batch_labels,
|
|
590
|
+
offset,
|
|
591
|
+
):
|
|
592
|
+
for layer in self.layers: # type: ignore
|
|
593
|
+
for head in range(self.num_heads): # type: ignore
|
|
594
|
+
tensor = torch.stack(
|
|
595
|
+
[
|
|
596
|
+
attention_matrices[layer - 1, i, head]
|
|
597
|
+
for i in range(len(batch_labels))
|
|
598
|
+
]
|
|
599
|
+
)
|
|
600
|
+
if self.flatten:
|
|
601
|
+
tensor = tensor.flatten(start_dim=1)
|
|
602
|
+
if self.streaming_output:
|
|
603
|
+
# output_file = self.attention_matrices_all_heads["output_data"][
|
|
604
|
+
# layer
|
|
605
|
+
# ][head]
|
|
606
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
607
|
+
self.io_dispatcher.enqueue(
|
|
608
|
+
output_type="attention_matrices_all_heads",
|
|
609
|
+
layer=layer,
|
|
610
|
+
head=head,
|
|
611
|
+
offset=offset,
|
|
612
|
+
array=np.ascontiguousarray(
|
|
613
|
+
tensor.cpu().numpy()
|
|
614
|
+
), # Ensure it's on CPU and NumPy
|
|
615
|
+
)
|
|
616
|
+
else:
|
|
617
|
+
self.attention_head["output_data"][layer][
|
|
618
|
+
head
|
|
619
|
+
].extend(tensor)
|
|
620
|
+
|
|
621
|
+
def _extract_attention_layer(
|
|
622
|
+
self,
|
|
623
|
+
attention_matrices,
|
|
624
|
+
batch_labels,
|
|
625
|
+
offset,
|
|
626
|
+
):
|
|
627
|
+
for layer in self.layers: # type: ignore
|
|
628
|
+
tensor = torch.stack(
|
|
629
|
+
[
|
|
630
|
+
attention_matrices[layer - 1, i].mean(0)
|
|
631
|
+
for i in range(len(batch_labels))
|
|
632
|
+
]
|
|
633
|
+
)
|
|
634
|
+
if self.flatten:
|
|
635
|
+
tensor = tensor.flatten(start_dim=1)
|
|
636
|
+
if self.streaming_output:
|
|
637
|
+
# output_file = self.attention_matrices_average_layers["output_data"][
|
|
638
|
+
# layer
|
|
639
|
+
# ]
|
|
640
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
641
|
+
self.io_dispatcher.enqueue(
|
|
642
|
+
output_type="attention_matrices_average_layers",
|
|
643
|
+
layer=layer,
|
|
644
|
+
head=None,
|
|
645
|
+
offset=offset,
|
|
646
|
+
array=self._to_numpy(tensor), # Ensure it's on CPU and NumPy
|
|
647
|
+
)
|
|
648
|
+
else:
|
|
649
|
+
self.attention_layer["output_data"][layer].extend(
|
|
650
|
+
tensor
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def _extract_attention_model(
|
|
654
|
+
self,
|
|
655
|
+
attention_matrices,
|
|
656
|
+
batch_labels,
|
|
657
|
+
offset,
|
|
658
|
+
):
|
|
659
|
+
tensor = torch.stack(
|
|
660
|
+
[
|
|
661
|
+
attention_matrices[:, i].mean(dim=(0, 1))
|
|
662
|
+
for i in range(len(batch_labels))
|
|
663
|
+
]
|
|
664
|
+
)
|
|
665
|
+
if self.flatten:
|
|
666
|
+
tensor = tensor.flatten(start_dim=1)
|
|
667
|
+
if self.streaming_output:
|
|
668
|
+
# output_file = self.attention_matrices_average_all["output_data"]
|
|
669
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
670
|
+
self.io_dispatcher.enqueue(
|
|
671
|
+
output_type="attention_matrices_average_all",
|
|
672
|
+
layer=None,
|
|
673
|
+
head=None,
|
|
674
|
+
offset=offset,
|
|
675
|
+
array=np.ascontiguousarray(
|
|
676
|
+
tensor.cpu().numpy()
|
|
677
|
+
), # Ensure it's on CPU and NumPy
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
self.attention_model["output_data"].extend(tensor)
|
|
681
|
+
|
|
682
|
+
def _extract_substring_pooled(
|
|
683
|
+
self,
|
|
684
|
+
representations,
|
|
685
|
+
substring_mask,
|
|
686
|
+
offset,
|
|
687
|
+
):
|
|
688
|
+
for layer in self.layers: # type: ignore
|
|
689
|
+
tensor = torch.stack(
|
|
690
|
+
[
|
|
691
|
+
(
|
|
692
|
+
(mask.unsqueeze(-1) * representations[layer][i]).sum(0)
|
|
693
|
+
/ mask.unsqueeze(-1).sum(0)
|
|
694
|
+
)
|
|
695
|
+
for i, mask in enumerate(substring_mask)
|
|
696
|
+
]
|
|
697
|
+
)
|
|
698
|
+
if self.streaming_output:
|
|
699
|
+
# output_file = self.substring_pooled["output_data"][layer]
|
|
700
|
+
# self.write_batch_to_disk(output_file, tensor, offset)
|
|
701
|
+
self.io_dispatcher.enqueue(
|
|
702
|
+
output_type="substring_pooled",
|
|
703
|
+
layer=layer,
|
|
704
|
+
head=None,
|
|
705
|
+
offset=offset,
|
|
706
|
+
array=self._to_numpy(tensor), # Ensure it's on CPU and NumPy
|
|
707
|
+
)
|
|
708
|
+
else:
|
|
709
|
+
self.substring_pooled["output_data"][layer].extend(tensor)
|
|
710
|
+
|
|
711
|
+
def _prepare_tensor(self, data_list, flatten):
|
|
712
|
+
tensor = torch.stack(data_list, dim=0)
|
|
713
|
+
if flatten:
|
|
714
|
+
tensor = tensor.flatten(start_dim=1)
|
|
715
|
+
return tensor.numpy()
|
|
716
|
+
|
|
717
|
+
def _to_numpy(self, t: torch.Tensor) -> np.ndarray:
|
|
718
|
+
return t.detach().cpu().contiguous().numpy()
|
|
719
|
+
|
|
720
|
+
def export_to_disk(self):
|
|
721
|
+
for output_type in self.output_types:
|
|
722
|
+
logger.info(f"Saving {output_type} representations...")
|
|
723
|
+
|
|
724
|
+
output_data = getattr(self, output_type)["output_data"]
|
|
725
|
+
output_dir = getattr(self, output_type)["output_dir"]
|
|
726
|
+
|
|
727
|
+
if isinstance(output_data, dict):
|
|
728
|
+
for layer in self.layers: # type: ignore
|
|
729
|
+
if isinstance(output_data[layer], dict): # e.g., attention_head
|
|
730
|
+
for head in range(self.num_heads): # type: ignore
|
|
731
|
+
tensor = self._prepare_tensor(
|
|
732
|
+
output_data[layer][head], self.flatten
|
|
733
|
+
)
|
|
734
|
+
file_path = self._make_output_filepath(
|
|
735
|
+
output_type, output_dir, layer, head
|
|
736
|
+
)
|
|
737
|
+
np.save(file_path, tensor)
|
|
738
|
+
logger.info(
|
|
739
|
+
f"Saved {output_type} layer {layer} head {head + 1} to {file_path}"
|
|
740
|
+
)
|
|
741
|
+
else:
|
|
742
|
+
# Handle layer-based outputs (mean_pooled, per_token, substring_pooled, attention_layer, logits)
|
|
743
|
+
flatten = self.flatten and output_type == "per_token"
|
|
744
|
+
tensor = self._prepare_tensor(output_data[layer], flatten)
|
|
745
|
+
file_path = self._make_output_filepath(
|
|
746
|
+
output_type, output_dir, layer
|
|
747
|
+
)
|
|
748
|
+
np.save(file_path, tensor)
|
|
749
|
+
logger.info(f"Saved {output_type} layer {layer} to {file_path}")
|
|
750
|
+
else:
|
|
751
|
+
# Handle model-level outputs (attention_model)
|
|
752
|
+
tensor = self._prepare_tensor(output_data, self.flatten)
|
|
753
|
+
file_path = self._make_output_filepath(output_type, output_dir)
|
|
754
|
+
np.save(file_path, tensor)
|
|
755
|
+
logger.info(f"Saved {output_type} to {file_path}")
|
|
756
|
+
|
|
757
|
+
def export_sequence_indices(self):
|
|
758
|
+
"""Save sequence indices to a CSV file."""
|
|
759
|
+
input_file_name = os.path.basename(self.fasta_path)
|
|
760
|
+
# replace file extension with _idx.csv regardless of pattern
|
|
761
|
+
output_file_name = os.path.splitext(input_file_name)[0] + "_idx.csv"
|
|
762
|
+
output_file_idx = os.path.join(self.output_path, output_file_name)
|
|
763
|
+
with open(output_file_idx, "w") as f:
|
|
764
|
+
f.write("index,sequence_id\n")
|
|
765
|
+
for i, label in enumerate(self.sequence_labels):
|
|
766
|
+
f.write(f"{i},{label}\n")
|
|
767
|
+
logger.info(f"Saved sequence indices to {output_file_idx}")
|
|
768
|
+
|
|
769
|
+
def _create_output_dirs(self):
|
|
770
|
+
for output_type in self.output_types:
|
|
771
|
+
output_type_path = os.path.join(self.output_path, output_type)
|
|
772
|
+
if not os.path.exists(output_type_path):
|
|
773
|
+
os.makedirs(output_type_path)
|
|
774
|
+
|
|
775
|
+
def run(self):
|
|
776
|
+
self._create_output_dirs()
|
|
777
|
+
if self.streaming_output:
|
|
778
|
+
logger.info("Preallocating disk space...")
|
|
779
|
+
self.memmap_registry = self.preallocate_disk_space()
|
|
780
|
+
logger.info("Preallocated disk space")
|
|
781
|
+
logger.info("Created output directories")
|
|
782
|
+
|
|
783
|
+
logger.info("Start embedding extraction")
|
|
784
|
+
self.embed()
|
|
785
|
+
logger.info("Finished embedding extraction")
|
|
786
|
+
|
|
787
|
+
logger.info("Saving embeddings...")
|
|
788
|
+
if not self.streaming_output:
|
|
789
|
+
self.export_to_disk()
|
|
790
|
+
|
|
791
|
+
self.export_sequence_indices()
|
|
792
|
+
|
|
793
|
+
# Final cleanup of checkpoint file (in case embed() didn't handle it)
|
|
794
|
+
if self.streaming_output:
|
|
795
|
+
self._cleanup_checkpoint()
|
|
796
|
+
|
|
797
|
+
logger.info("Pipeline completed successfully!")
|