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.
@@ -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!")