AbstractPhil commited on
Commit
6413528
·
verified ·
1 Parent(s): 5e3ce26

Create cell1_prepare_data.py

Browse files
Files changed (1) hide show
  1. cell1_prepare_data.py +960 -0
cell1_prepare_data.py ADDED
@@ -0,0 +1,960 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # GEOLIP-BERTENSTEIN STAGE 1: MULTI-EXPERT PRECOMPUTE (REFACTORED)
3
+ #
4
+ # BERT is the shared text spine.
5
+ #
6
+ # Pipeline per expert pair:
7
+ # 1. Load dataset / stream
8
+ # 2. CPU preprocess text + expert input
9
+ # 3. GPU encode text with BERT + expert with expert encoder
10
+ # 4. Shard-safe Arrow write
11
+ # 5. Merge shards -> final save_to_disk
12
+ # 6. Unload expert, keep BERT
13
+ #
14
+ # Experts:
15
+ # image : DINOv2-large + COCO-Caption
16
+ # audio : Whisper-large + LibriSpeech ASR (streaming)
17
+ # protein : ESM-2-650M + Protein2Text-QA (streaming)
18
+ # code : CodeBERT-base + CodeSearchNet python
19
+ # ============================================================================
20
+
21
+ import subprocess
22
+ import sys
23
+
24
+ try:
25
+ import sympy
26
+ _ = sympy.core
27
+ except (ImportError, AttributeError):
28
+ subprocess.check_call(
29
+ [sys.executable, "-m", "pip", "install", "--upgrade", "sympy", "--break-system-packages", "-q"]
30
+ )
31
+
32
+ import gc
33
+ import os
34
+ import shutil
35
+ import time
36
+ from dataclasses import dataclass
37
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
38
+
39
+ import numpy as np
40
+ import torch
41
+ from torch.utils.data import Dataset, DataLoader
42
+ from datasets import (
43
+ Audio,
44
+ Dataset as HFDataset,
45
+ Features,
46
+ Sequence,
47
+ Value,
48
+ Array2D,
49
+ concatenate_datasets,
50
+ load_dataset,
51
+ load_from_disk,
52
+ )
53
+
54
+ # ============================================================================
55
+ # BASE CONFIG
56
+ # ============================================================================
57
+
58
+ @dataclass
59
+ class BaseConfig:
60
+ cache_dir: str = "/home/claude/geo_cache"
61
+ max_text_len: int = 32
62
+
63
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
64
+ amp_enabled: bool = torch.cuda.is_available()
65
+
66
+ bert_model_name: str = "google-bert/bert-large-uncased"
67
+ bert_hidden_dim: int = 1024
68
+
69
+ batch_size: int = 256
70
+ num_workers: int = 8
71
+ prefetch_factor: int = 2
72
+ pin_memory: bool = torch.cuda.is_available()
73
+
74
+ shard_size_default: int = 2048
75
+
76
+ # expert-specific max samples
77
+ max_audio_samples: int = 10000
78
+ max_protein_samples: int = 15000
79
+ max_code_samples: int = 50000
80
+
81
+ cleanup_hf_cache_between_experts: bool = True
82
+
83
+
84
+ CFG = BaseConfig()
85
+ DEVICE = torch.device(CFG.device)
86
+
87
+
88
+ # ============================================================================
89
+ # HF CACHE CLEANUP
90
+ # ============================================================================
91
+
92
+ def cleanup_hf_cache() -> None:
93
+ """Delete HF datasets/hub cache to free disk after encoding an expert."""
94
+ hf_cache = os.path.expanduser("~/.cache/huggingface")
95
+ for subdir in ["datasets", "hub"]:
96
+ p = os.path.join(hf_cache, subdir)
97
+ if not os.path.exists(p):
98
+ continue
99
+
100
+ size_gb = 0.0
101
+ for dp, _, files in os.walk(p):
102
+ for f in files:
103
+ fp = os.path.join(dp, f)
104
+ try:
105
+ size_gb += os.path.getsize(fp)
106
+ except OSError:
107
+ pass
108
+ size_gb /= 1e9
109
+
110
+ print(f" Cleaning {p} ({size_gb:.1f} GB)...")
111
+ shutil.rmtree(p, ignore_errors=True)
112
+ os.makedirs(p, exist_ok=True)
113
+
114
+
115
+ def cleanup_cuda() -> None:
116
+ gc.collect()
117
+ if torch.cuda.is_available():
118
+ torch.cuda.empty_cache()
119
+
120
+
121
+ # ============================================================================
122
+ # SHARED BERT
123
+ # ============================================================================
124
+
125
+ _bert_tokenizer = None
126
+
127
+ def get_bert_tokenizer():
128
+ global _bert_tokenizer
129
+ if _bert_tokenizer is None:
130
+ from transformers import BertTokenizer
131
+ _bert_tokenizer = BertTokenizer.from_pretrained(CFG.bert_model_name)
132
+ return _bert_tokenizer
133
+
134
+
135
+ def load_shared_bert():
136
+ from transformers import BertModel
137
+ print("Loading shared BERT-large...")
138
+ bert = BertModel.from_pretrained(
139
+ CFG.bert_model_name,
140
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
141
+ ).to(DEVICE).eval()
142
+ print(" BERT ready.")
143
+ return bert
144
+
145
+
146
+ # ============================================================================
147
+ # COMMON HELPERS
148
+ # ============================================================================
149
+
150
+ def ensure_dir(path: str) -> None:
151
+ os.makedirs(path, exist_ok=True)
152
+
153
+
154
+ def make_loader(ds: Dataset, batch_size: int, num_workers: int) -> DataLoader:
155
+ kwargs = dict(
156
+ dataset=ds,
157
+ batch_size=batch_size,
158
+ shuffle=False,
159
+ num_workers=num_workers,
160
+ pin_memory=CFG.pin_memory,
161
+ persistent_workers=num_workers > 0,
162
+ )
163
+ if num_workers > 0:
164
+ kwargs["prefetch_factor"] = CFG.prefetch_factor
165
+ return DataLoader(**kwargs)
166
+
167
+
168
+ def masked_text_tokenize(text: str, tokenizer) -> Tuple[torch.Tensor, torch.Tensor]:
169
+ tok = tokenizer(
170
+ text,
171
+ padding="max_length",
172
+ truncation=True,
173
+ max_length=CFG.max_text_len,
174
+ return_tensors="pt",
175
+ )
176
+ return tok["input_ids"].squeeze(0), tok["attention_mask"].squeeze(0)
177
+
178
+
179
+ def extract_first_text(sample: Dict[str, Any], keys: List[str]) -> str:
180
+ for key in keys:
181
+ if key not in sample:
182
+ continue
183
+ value = sample[key]
184
+
185
+ if isinstance(value, str):
186
+ value = value.strip()
187
+ if value:
188
+ return value
189
+
190
+ if isinstance(value, list) and value:
191
+ first = value[0]
192
+ if isinstance(first, str):
193
+ first = first.strip()
194
+ if first:
195
+ return first
196
+ if isinstance(first, dict):
197
+ txt = str(first.get("raw", first.get("text", ""))).strip()
198
+ if txt:
199
+ return txt
200
+ txt = str(first).strip()
201
+ if txt:
202
+ return txt
203
+
204
+ return ""
205
+
206
+
207
+ # ============================================================================
208
+ # SHARD WRITER
209
+ # ============================================================================
210
+
211
+ class ShardWriter:
212
+ def __init__(
213
+ self,
214
+ cache_dir: str,
215
+ tag: str,
216
+ features: Features,
217
+ shard_size: int,
218
+ row_keys: List[str],
219
+ ):
220
+ self.cache_dir = cache_dir
221
+ self.tag = tag
222
+ self.features = features
223
+ self.shard_size = shard_size
224
+ self.row_keys = row_keys
225
+
226
+ self.cache_path = os.path.join(cache_dir, tag)
227
+ self.shard_root = os.path.join(cache_dir, f"{tag}__shards")
228
+
229
+ self.rows = {k: [] for k in row_keys}
230
+ self.shard_paths: List[str] = []
231
+ self.shard_idx = 0
232
+ self.n_written = 0
233
+
234
+ @property
235
+ def exists(self) -> bool:
236
+ return os.path.exists(self.cache_path)
237
+
238
+ def add_row(self, row: Dict[str, Any]) -> None:
239
+ for k in self.row_keys:
240
+ self.rows[k].append(row[k])
241
+
242
+ if len(self.rows[self.row_keys[0]]) >= self.shard_size:
243
+ self.flush()
244
+
245
+ def flush(self) -> None:
246
+ n_rows = len(self.rows[self.row_keys[0]])
247
+ if n_rows == 0:
248
+ return
249
+
250
+ ensure_dir(self.shard_root)
251
+ shard_path = os.path.join(self.shard_root, f"shard_{self.shard_idx:05d}")
252
+ ds = HFDataset.from_dict(self.rows, features=self.features)
253
+ ds.save_to_disk(shard_path)
254
+
255
+ self.shard_paths.append(shard_path)
256
+ self.shard_idx += 1
257
+ self.n_written += n_rows
258
+ self.rows = {k: [] for k in self.row_keys}
259
+
260
+ def finalize(self) -> str:
261
+ self.flush()
262
+
263
+ print(f" Merging {len(self.shard_paths)} shards...")
264
+ merged = concatenate_datasets([load_from_disk(p) for p in self.shard_paths])
265
+ merged.save_to_disk(self.cache_path)
266
+ print(f" Saved {len(merged)} pairs to {self.cache_path}")
267
+
268
+ if os.path.exists(self.shard_root):
269
+ shutil.rmtree(self.shard_root, ignore_errors=True)
270
+
271
+ return self.cache_path
272
+
273
+
274
+ # ============================================================================
275
+ # MAP-STYLE DATASETS (NON-STREAMING)
276
+ # ============================================================================
277
+
278
+ class ImageTextDataset(Dataset):
279
+ def __init__(self, hf_ds, bert_tokenizer, image_processor):
280
+ self.ds = hf_ds
281
+ self.tok = bert_tokenizer
282
+ self.proc = image_processor
283
+ self.fallback_shape = (3, 518, 518)
284
+
285
+ def __len__(self):
286
+ return len(self.ds)
287
+
288
+ def __getitem__(self, idx):
289
+ sample = self.ds[idx]
290
+
291
+ caption = extract_first_text(
292
+ sample,
293
+ ["answer", "caption", "captions", "text", "original_alt_text"],
294
+ )
295
+ ids, mask = masked_text_tokenize(caption, self.tok)
296
+
297
+ image = sample.get("image", None)
298
+ valid = True
299
+
300
+ if image is not None and hasattr(image, "convert"):
301
+ try:
302
+ expert_input = self.proc(
303
+ images=image.convert("RGB"),
304
+ return_tensors="pt",
305
+ )["pixel_values"].squeeze(0)
306
+ except Exception:
307
+ expert_input = torch.zeros(self.fallback_shape, dtype=torch.float32)
308
+ valid = False
309
+ else:
310
+ expert_input = torch.zeros(self.fallback_shape, dtype=torch.float32)
311
+ valid = False
312
+
313
+ return ids, mask, expert_input, valid
314
+
315
+
316
+ class CodeTextDataset(Dataset):
317
+ def __init__(self, hf_ds, bert_tokenizer, code_tokenizer):
318
+ self.ds = hf_ds
319
+ self.tok = bert_tokenizer
320
+ self.code_tok = code_tokenizer
321
+
322
+ def __len__(self):
323
+ return len(self.ds)
324
+
325
+ def __getitem__(self, idx):
326
+ sample = self.ds[idx]
327
+
328
+ doc = sample.get("func_documentation_string", "")
329
+ if not doc or not doc.strip():
330
+ doc = str(sample.get("whole_func_string", ""))[:200]
331
+ doc = str(doc).strip()[:500]
332
+
333
+ ids, mask = masked_text_tokenize(doc, self.tok)
334
+
335
+ code = str(sample.get("func_code_string", sample.get("whole_func_string", ""))).strip()[:512]
336
+ valid = len(code) > 5 and len(doc) > 5
337
+
338
+ if valid:
339
+ try:
340
+ tok = self.code_tok(
341
+ code,
342
+ padding="max_length",
343
+ truncation=True,
344
+ max_length=256,
345
+ return_tensors="pt",
346
+ )
347
+ code_ids = tok["input_ids"].squeeze(0)
348
+ code_mask = tok["attention_mask"].squeeze(0)
349
+ except Exception:
350
+ code_ids = torch.zeros(256, dtype=torch.long)
351
+ code_mask = torch.zeros(256, dtype=torch.long)
352
+ valid = False
353
+ else:
354
+ code_ids = torch.zeros(256, dtype=torch.long)
355
+ code_mask = torch.zeros(256, dtype=torch.long)
356
+
357
+ return ids, mask, torch.stack([code_ids, code_mask]), valid
358
+
359
+
360
+ # ============================================================================
361
+ # SHARED NON-STREAM ENCODER
362
+ # ============================================================================
363
+
364
+ @torch.no_grad()
365
+ def encode_map_dataset(
366
+ *,
367
+ tag: str,
368
+ loader: DataLoader,
369
+ bert,
370
+ expert_name: str,
371
+ expert_hidden_shape: Tuple[int, int],
372
+ expert_forward: Callable[[torch.Tensor], torch.Tensor],
373
+ shard_size: int,
374
+ max_samples: Optional[int] = None,
375
+ ) -> str:
376
+ cache_path = os.path.join(CFG.cache_dir, tag)
377
+ if os.path.exists(cache_path):
378
+ print(f" Cache exists: {cache_path}")
379
+ return cache_path
380
+
381
+ features = Features({
382
+ "text_hidden": Array2D(shape=(CFG.max_text_len, CFG.bert_hidden_dim), dtype="float16"),
383
+ "text_mask": Sequence(Value("bool"), length=CFG.max_text_len),
384
+ f"{expert_name}_hidden": Array2D(shape=expert_hidden_shape, dtype="float16"),
385
+ })
386
+
387
+ writer = ShardWriter(
388
+ cache_dir=CFG.cache_dir,
389
+ tag=tag,
390
+ features=features,
391
+ shard_size=shard_size,
392
+ row_keys=["text_hidden", "text_mask", f"{expert_name}_hidden"],
393
+ )
394
+
395
+ t0 = time.time()
396
+ n = 0
397
+
398
+ for batch in loader:
399
+ text_ids, text_mask, expert_input, valid = batch
400
+ valid_b = valid.bool()
401
+
402
+ if not valid_b.any():
403
+ continue
404
+
405
+ text_ids = text_ids[valid_b].to(DEVICE, non_blocking=True)
406
+ text_mask_gpu = text_mask[valid_b].to(DEVICE, non_blocking=True)
407
+ expert_input = expert_input[valid_b].to(DEVICE, non_blocking=True)
408
+
409
+ text_hidden = bert(
410
+ input_ids=text_ids,
411
+ attention_mask=text_mask_gpu,
412
+ ).last_hidden_state.detach().to(dtype=torch.float16).cpu().numpy()
413
+
414
+ text_mask_np = text_mask_gpu.bool().cpu().numpy()
415
+ expert_hidden = expert_forward(expert_input).detach().to(dtype=torch.float16).cpu().numpy()
416
+
417
+ for i in range(text_hidden.shape[0]):
418
+ writer.add_row({
419
+ "text_hidden": text_hidden[i],
420
+ "text_mask": text_mask_np[i].tolist(),
421
+ f"{expert_name}_hidden": expert_hidden[i],
422
+ })
423
+
424
+ n += text_hidden.shape[0]
425
+ if n % 1000 < CFG.batch_size or n <= CFG.batch_size:
426
+ rate = n / max(time.time() - t0, 1e-6)
427
+ print(f" {n}" + (f"/{max_samples}" if max_samples else "") + f" ({rate:.0f}/s)")
428
+
429
+ if max_samples is not None and n >= max_samples:
430
+ break
431
+
432
+ final_path = writer.finalize()
433
+ print(f" Completed {n} samples in {time.time() - t0:.0f}s")
434
+ return final_path
435
+
436
+
437
+ # ============================================================================
438
+ # STREAMING HELPERS
439
+ # ============================================================================
440
+
441
+ def decode_audio_obj(audio_obj) -> Tuple[np.ndarray, int]:
442
+ if hasattr(audio_obj, "get_all_samples"):
443
+ samples = audio_obj.get_all_samples()
444
+ arr = samples.data.numpy().squeeze()
445
+ sr = samples.sample_rate
446
+ return arr, sr
447
+
448
+ if isinstance(audio_obj, dict):
449
+ return audio_obj["array"], audio_obj.get("sampling_rate", 16000)
450
+
451
+ raise TypeError(f"Unsupported audio object type: {type(audio_obj)}")
452
+
453
+
454
+ def stream_librispeech_batches(
455
+ stream,
456
+ bert_tokenizer,
457
+ whisper_processor,
458
+ batch_size: int,
459
+ ) -> Iterable[Tuple[torch.Tensor, torch.Tensor, torch.Tensor]]:
460
+ batch_ids = []
461
+ batch_masks = []
462
+ batch_mels = []
463
+
464
+ for sample in stream:
465
+ text = sample.get("text", sample.get("transcription", ""))
466
+ audio_obj = sample.get("audio")
467
+ if not text or audio_obj is None:
468
+ continue
469
+
470
+ try:
471
+ audio_array, sr = decode_audio_obj(audio_obj)
472
+ except Exception:
473
+ continue
474
+
475
+ ids, mask = masked_text_tokenize(str(text), bert_tokenizer)
476
+
477
+ try:
478
+ mel = whisper_processor(
479
+ audio_array,
480
+ sampling_rate=sr,
481
+ return_tensors="pt",
482
+ ).input_features.squeeze(0)
483
+ except Exception:
484
+ continue
485
+
486
+ batch_ids.append(ids)
487
+ batch_masks.append(mask)
488
+ batch_mels.append(mel)
489
+
490
+ if len(batch_ids) >= batch_size:
491
+ yield (
492
+ torch.stack(batch_ids),
493
+ torch.stack(batch_masks),
494
+ torch.stack(batch_mels),
495
+ )
496
+ batch_ids, batch_masks, batch_mels = [], [], []
497
+
498
+ if batch_ids:
499
+ yield (
500
+ torch.stack(batch_ids),
501
+ torch.stack(batch_masks),
502
+ torch.stack(batch_mels),
503
+ )
504
+
505
+
506
+ def extract_protein_caption(sample: Dict[str, Any]) -> str:
507
+ convos = sample.get("conversations", [])
508
+ if isinstance(convos, list):
509
+ for c in convos:
510
+ if isinstance(c, dict) and c.get("from") == "gpt":
511
+ v = str(c.get("value", "")).strip()
512
+ if v:
513
+ return v[:500]
514
+ for c in convos:
515
+ if isinstance(c, dict) and "value" in c:
516
+ v = str(c["value"]).strip()
517
+ if v:
518
+ return v[:500]
519
+
520
+ return str(sample.get("protein", "")).strip()[:500]
521
+
522
+
523
+ def stream_protein_batches(
524
+ stream,
525
+ bert_tokenizer,
526
+ esm_tokenizer,
527
+ batch_size: int,
528
+ ) -> Iterable[Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]]:
529
+ batch_ids = []
530
+ batch_masks = []
531
+ batch_esm_ids = []
532
+ batch_esm_masks = []
533
+
534
+ for sample in stream:
535
+ caption = extract_protein_caption(sample)
536
+ seq = str(sample.get("amino_seq", sample.get("protein_sequence", ""))).strip()
537
+
538
+ if len(caption) < 5 or len(seq) < 5:
539
+ continue
540
+
541
+ ids, mask = masked_text_tokenize(caption, bert_tokenizer)
542
+
543
+ try:
544
+ esm_t = esm_tokenizer(
545
+ seq,
546
+ padding="max_length",
547
+ truncation=True,
548
+ max_length=512,
549
+ return_tensors="pt",
550
+ )
551
+ except Exception:
552
+ continue
553
+
554
+ batch_ids.append(ids)
555
+ batch_masks.append(mask)
556
+ batch_esm_ids.append(esm_t["input_ids"].squeeze(0))
557
+ batch_esm_masks.append(esm_t["attention_mask"].squeeze(0))
558
+
559
+ if len(batch_ids) >= batch_size:
560
+ yield (
561
+ torch.stack(batch_ids),
562
+ torch.stack(batch_masks),
563
+ torch.stack(batch_esm_ids),
564
+ torch.stack(batch_esm_masks),
565
+ )
566
+ batch_ids, batch_masks, batch_esm_ids, batch_esm_masks = [], [], [], []
567
+
568
+ if batch_ids:
569
+ yield (
570
+ torch.stack(batch_ids),
571
+ torch.stack(batch_masks),
572
+ torch.stack(batch_esm_ids),
573
+ torch.stack(batch_esm_masks),
574
+ )
575
+
576
+
577
+ @torch.no_grad()
578
+ def encode_streaming_batches(
579
+ *,
580
+ tag: str,
581
+ expert_name: str,
582
+ expert_hidden_shape: Tuple[int, int],
583
+ batch_iter: Iterable,
584
+ bert,
585
+ expert_batch_forward: Callable[..., torch.Tensor],
586
+ shard_size: int,
587
+ row_keys: List[str],
588
+ max_samples: Optional[int] = None,
589
+ ) -> str:
590
+ cache_path = os.path.join(CFG.cache_dir, tag)
591
+ if os.path.exists(cache_path):
592
+ print(f" Cache exists: {cache_path}")
593
+ return cache_path
594
+
595
+ features = Features({
596
+ "text_hidden": Array2D(shape=(CFG.max_text_len, CFG.bert_hidden_dim), dtype="float16"),
597
+ "text_mask": Sequence(Value("bool"), length=CFG.max_text_len),
598
+ f"{expert_name}_hidden": Array2D(shape=expert_hidden_shape, dtype="float16"),
599
+ })
600
+
601
+ writer = ShardWriter(
602
+ cache_dir=CFG.cache_dir,
603
+ tag=tag,
604
+ features=features,
605
+ shard_size=shard_size,
606
+ row_keys=row_keys,
607
+ )
608
+
609
+ t0 = time.time()
610
+ n = 0
611
+
612
+ for packed in batch_iter:
613
+ # first two are always bert ids/masks
614
+ text_ids = packed[0].to(DEVICE, non_blocking=True)
615
+ text_mask = packed[1].to(DEVICE, non_blocking=True)
616
+
617
+ text_hidden = bert(
618
+ input_ids=text_ids,
619
+ attention_mask=text_mask,
620
+ ).last_hidden_state.detach().to(dtype=torch.float16).cpu().numpy()
621
+
622
+ text_mask_np = text_mask.bool().cpu().numpy()
623
+
624
+ expert_hidden = expert_batch_forward(*[p.to(DEVICE, non_blocking=True) for p in packed[2:]])
625
+ expert_hidden = expert_hidden.detach().to(dtype=torch.float16).cpu().numpy()
626
+
627
+ for i in range(text_hidden.shape[0]):
628
+ writer.add_row({
629
+ "text_hidden": text_hidden[i],
630
+ "text_mask": text_mask_np[i].tolist(),
631
+ f"{expert_name}_hidden": expert_hidden[i],
632
+ })
633
+
634
+ n += text_hidden.shape[0]
635
+ batch_size = text_hidden.shape[0]
636
+ if n % 1000 < batch_size or n <= batch_size:
637
+ rate = n / max(time.time() - t0, 1e-6)
638
+ print(f" {n}" + (f"/{max_samples}" if max_samples else "") + f" ({rate:.0f}/s)")
639
+
640
+ if max_samples is not None and n >= max_samples:
641
+ break
642
+
643
+ final_path = writer.finalize()
644
+ print(f" Completed {n} samples in {time.time() - t0:.0f}s")
645
+ return final_path
646
+
647
+
648
+ # ============================================================================
649
+ # EXPERT RUNNERS
650
+ # ============================================================================
651
+
652
+ def encode_image_expert(bert, split: str, tag: str, max_samples: Optional[int] = None) -> str:
653
+ from transformers import Dinov2Model, AutoImageProcessor
654
+
655
+ print(f"\n [IMAGE] Loading DINOv2-large + COCO-Caption ({split})...")
656
+ dino = Dinov2Model.from_pretrained(
657
+ "facebook/dinov2-large",
658
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
659
+ ).to(DEVICE).eval()
660
+ proc = AutoImageProcessor.from_pretrained("facebook/dinov2-large")
661
+ tok = get_bert_tokenizer()
662
+
663
+ hf_ds = load_dataset("lmms-lab/COCO-Caption", split=split)
664
+ if max_samples is not None:
665
+ hf_ds = hf_ds.select(range(min(max_samples, len(hf_ds))))
666
+ print(f" Dataset: {len(hf_ds)} samples")
667
+
668
+ torch_ds = ImageTextDataset(hf_ds, tok, proc)
669
+ loader = make_loader(torch_ds, batch_size=CFG.batch_size, num_workers=CFG.num_workers)
670
+
671
+ def expert_forward(pixel_values):
672
+ return dino(pixel_values=pixel_values).last_hidden_state
673
+
674
+ path = encode_map_dataset(
675
+ tag=tag,
676
+ loader=loader,
677
+ bert=bert,
678
+ expert_name="image",
679
+ expert_hidden_shape=(257, 1024),
680
+ expert_forward=expert_forward,
681
+ shard_size=CFG.shard_size_default,
682
+ max_samples=max_samples,
683
+ )
684
+
685
+ del dino, proc, hf_ds, torch_ds, loader
686
+ cleanup_cuda()
687
+ return path
688
+
689
+
690
+ def encode_code_expert(bert, max_samples: Optional[int] = None) -> str:
691
+ from transformers import RobertaModel, RobertaTokenizer
692
+
693
+ print("\n [CODE] Loading CodeBERT + CodeSearchNet python...")
694
+ codebert = RobertaModel.from_pretrained(
695
+ "microsoft/codebert-base",
696
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
697
+ ).to(DEVICE).eval()
698
+ code_tok = RobertaTokenizer.from_pretrained("microsoft/codebert-base")
699
+ tok = get_bert_tokenizer()
700
+
701
+ hf_ds = load_dataset("code-search-net/code_search_net", "python", split="train")
702
+ if max_samples is not None:
703
+ hf_ds = hf_ds.select(range(min(max_samples, len(hf_ds))))
704
+
705
+ hf_ds = hf_ds.filter(
706
+ lambda x: bool(x.get("func_documentation_string", "").strip()),
707
+ num_proc=4,
708
+ )
709
+ print(f" Dataset: {len(hf_ds)} samples (after filtering)")
710
+
711
+ torch_ds = CodeTextDataset(hf_ds, tok, code_tok)
712
+ loader = make_loader(torch_ds, batch_size=CFG.batch_size, num_workers=CFG.num_workers)
713
+
714
+ def expert_forward(packed):
715
+ code_ids = packed[:, 0].long()
716
+ code_mask = packed[:, 1].long()
717
+ return codebert(input_ids=code_ids, attention_mask=code_mask).last_hidden_state
718
+
719
+ path = encode_map_dataset(
720
+ tag="code_csn",
721
+ loader=loader,
722
+ bert=bert,
723
+ expert_name="code",
724
+ expert_hidden_shape=(256, 768),
725
+ expert_forward=expert_forward,
726
+ shard_size=CFG.shard_size_default,
727
+ max_samples=max_samples,
728
+ )
729
+
730
+ del codebert, code_tok, hf_ds, torch_ds, loader
731
+ cleanup_cuda()
732
+ return path
733
+
734
+
735
+ def encode_audio_expert(bert, max_samples: Optional[int] = None) -> str:
736
+ from transformers import WhisperModel, WhisperFeatureExtractor
737
+
738
+ print("\n [AUDIO] Loading Whisper-large-v3 + LibriSpeech ASR (streaming)...")
739
+ whisper = WhisperModel.from_pretrained(
740
+ "openai/whisper-large-v3",
741
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
742
+ ).to(DEVICE).eval()
743
+ proc = WhisperFeatureExtractor.from_pretrained("openai/whisper-large-v3")
744
+ tok = get_bert_tokenizer()
745
+
746
+ max_n = max_samples or CFG.max_audio_samples
747
+ audio_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
748
+
749
+ # probe shape
750
+ probe_stream = load_dataset("openslr/librispeech_asr", "clean", split="train.100", streaming=True)
751
+ probe_stream = probe_stream.cast_column("audio", Audio(sampling_rate=16000))
752
+ first = next(iter(probe_stream))
753
+ arr, sr = decode_audio_obj(first["audio"])
754
+
755
+ mel = proc(arr, sampling_rate=sr, return_tensors="pt").input_features
756
+ mel = mel.to(device=DEVICE, dtype=audio_dtype)
757
+
758
+ with torch.no_grad():
759
+ probe_hidden = whisper.encoder(mel).last_hidden_state
760
+
761
+ seq_len, hidden_dim = probe_hidden.shape[1], probe_hidden.shape[2]
762
+ print(f" Whisper encoder output: ({seq_len}, {hidden_dim})")
763
+ del mel, probe_hidden
764
+
765
+ stream = load_dataset("openslr/librispeech_asr", "clean", split="train.100", streaming=True)
766
+ stream = stream.cast_column("audio", Audio(sampling_rate=16000))
767
+
768
+ batch_iter = stream_librispeech_batches(
769
+ stream=stream,
770
+ bert_tokenizer=tok,
771
+ whisper_processor=proc,
772
+ batch_size=16,
773
+ )
774
+
775
+ def expert_batch_forward(mels: torch.Tensor) -> torch.Tensor:
776
+ mels = mels.to(dtype=audio_dtype)
777
+ return whisper.encoder(mels).last_hidden_state
778
+
779
+ path = encode_streaming_batches(
780
+ tag="audio_librispeech",
781
+ expert_name="audio",
782
+ expert_hidden_shape=(seq_len, hidden_dim),
783
+ batch_iter=batch_iter,
784
+ bert=bert,
785
+ expert_batch_forward=expert_batch_forward,
786
+ shard_size=256, # large hidden size; keep shards smaller
787
+ row_keys=["text_hidden", "text_mask", "audio_hidden"],
788
+ max_samples=max_n,
789
+ )
790
+
791
+ del whisper, proc
792
+ cleanup_cuda()
793
+ return path
794
+
795
+
796
+ def encode_protein_expert(bert, max_samples: Optional[int] = None) -> str:
797
+ from transformers import EsmModel, EsmTokenizer
798
+
799
+ print("\n [PROTEIN] Loading ESM-2-650M + Protein2Text-QA (streaming)...")
800
+ esm = EsmModel.from_pretrained(
801
+ "facebook/esm2_t33_650M_UR50D",
802
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
803
+ ).to(DEVICE).eval()
804
+ esm_tok = EsmTokenizer.from_pretrained("facebook/esm2_t33_650M_UR50D")
805
+ tok = get_bert_tokenizer()
806
+
807
+ max_n = max_samples or CFG.max_protein_samples
808
+ stream = load_dataset("tumorailab/Protein2Text-QA", split="test", streaming=True)
809
+
810
+ batch_iter = stream_protein_batches(
811
+ stream=stream,
812
+ bert_tokenizer=tok,
813
+ esm_tokenizer=esm_tok,
814
+ batch_size=32,
815
+ )
816
+
817
+ def expert_batch_forward(esm_ids: torch.Tensor, esm_mask: torch.Tensor) -> torch.Tensor:
818
+ return esm(input_ids=esm_ids.long(), attention_mask=esm_mask.long()).last_hidden_state
819
+
820
+ path = encode_streaming_batches(
821
+ tag="protein_p2t",
822
+ expert_name="protein",
823
+ expert_hidden_shape=(512, 1280),
824
+ batch_iter=batch_iter,
825
+ bert=bert,
826
+ expert_batch_forward=expert_batch_forward,
827
+ shard_size=512,
828
+ row_keys=["text_hidden", "text_mask", "protein_hidden"],
829
+ max_samples=max_n,
830
+ )
831
+
832
+ del esm, esm_tok
833
+ cleanup_cuda()
834
+ return path
835
+
836
+
837
+ # ============================================================================
838
+ # MAIN
839
+ # ============================================================================
840
+
841
+ def main():
842
+ ensure_dir(CFG.cache_dir)
843
+
844
+ print("=" * 70)
845
+ print("STAGE 1: MULTI-EXPERT PRECOMPUTE")
846
+ print("=" * 70)
847
+
848
+ if torch.cuda.is_available():
849
+ print(f"GPU: {torch.cuda.get_device_name()}")
850
+ print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
851
+ print(f"Cache: {CFG.cache_dir}")
852
+
853
+ required_tags = [
854
+ "image_coco",
855
+ "image_coco_test",
856
+ "audio_librispeech",
857
+ "protein_p2t",
858
+ "code_csn",
859
+ ]
860
+ missing = [t for t in required_tags if not os.path.exists(os.path.join(CFG.cache_dir, t))]
861
+
862
+ if not missing:
863
+ print("\nAll caches exist. Nothing to encode.")
864
+ bert = None
865
+ else:
866
+ print(f"\nMissing caches: {missing}")
867
+ if CFG.cleanup_hf_cache_between_experts:
868
+ cleanup_hf_cache()
869
+ bert = load_shared_bert()
870
+
871
+ paths: Dict[str, Optional[str]] = {}
872
+
873
+ # IMAGE TRAIN
874
+ print(f"\n{'─' * 50}")
875
+ print("[1/4] IMAGE — DINOv2 + COCO-Caption")
876
+ if os.path.exists(os.path.join(CFG.cache_dir, "image_coco")):
877
+ print(" [IMAGE] Cache exists, skipping.")
878
+ paths["image"] = os.path.join(CFG.cache_dir, "image_coco")
879
+ else:
880
+ paths["image"] = encode_image_expert(bert, split="val", tag="image_coco")
881
+ if CFG.cleanup_hf_cache_between_experts:
882
+ cleanup_hf_cache()
883
+
884
+ # IMAGE TEST
885
+ if os.path.exists(os.path.join(CFG.cache_dir, "image_coco_test")):
886
+ print("\n [IMAGE-TEST] Cache exists, skipping.")
887
+ paths["image_test"] = os.path.join(CFG.cache_dir, "image_coco_test")
888
+ else:
889
+ print("\n [IMAGE-TEST] COCO test split...")
890
+ paths["image_test"] = encode_image_expert(bert, split="test", tag="image_coco_test")
891
+ if CFG.cleanup_hf_cache_between_experts:
892
+ cleanup_hf_cache()
893
+
894
+ # AUDIO
895
+ print(f"\n{'─' * 50}")
896
+ print("[2/4] AUDIO — Whisper + LibriSpeech")
897
+ if os.path.exists(os.path.join(CFG.cache_dir, "audio_librispeech")):
898
+ print(" [AUDIO] Cache exists, skipping.")
899
+ paths["audio"] = os.path.join(CFG.cache_dir, "audio_librispeech")
900
+ else:
901
+ try:
902
+ paths["audio"] = encode_audio_expert(bert, max_samples=CFG.max_audio_samples)
903
+ except Exception as e:
904
+ print(f" AUDIO failed: {e}")
905
+ paths["audio"] = None
906
+ if CFG.cleanup_hf_cache_between_experts:
907
+ cleanup_hf_cache()
908
+
909
+ # PROTEIN
910
+ print(f"\n{'─' * 50}")
911
+ print("[3/4] PROTEIN — ESM-2 + Protein2Text-QA")
912
+ if os.path.exists(os.path.join(CFG.cache_dir, "protein_p2t")):
913
+ print(" [PROTEIN] Cache exists, skipping.")
914
+ paths["protein"] = os.path.join(CFG.cache_dir, "protein_p2t")
915
+ else:
916
+ try:
917
+ paths["protein"] = encode_protein_expert(bert, max_samples=CFG.max_protein_samples)
918
+ except Exception as e:
919
+ print(f" PROTEIN failed: {e}")
920
+ paths["protein"] = None
921
+ if CFG.cleanup_hf_cache_between_experts:
922
+ cleanup_hf_cache()
923
+
924
+ # CODE
925
+ print(f"\n{'─' * 50}")
926
+ print("[4/4] CODE — CodeBERT + CodeSearchNet Python")
927
+ if os.path.exists(os.path.join(CFG.cache_dir, "code_csn")):
928
+ print(" [CODE] Cache exists, skipping.")
929
+ paths["code"] = os.path.join(CFG.cache_dir, "code_csn")
930
+ else:
931
+ try:
932
+ paths["code"] = encode_code_expert(bert, max_samples=CFG.max_code_samples)
933
+ except Exception as e:
934
+ print(f" CODE failed: {e}")
935
+ paths["code"] = None
936
+ if CFG.cleanup_hf_cache_between_experts:
937
+ cleanup_hf_cache()
938
+
939
+ if bert is not None:
940
+ del bert
941
+ cleanup_cuda()
942
+
943
+ flickr_path = os.path.join(CFG.cache_dir, "flickr30k")
944
+ if os.path.exists(flickr_path):
945
+ paths["flickr"] = flickr_path
946
+
947
+ print(f"\n{'=' * 70}")
948
+ print("CACHE SUMMARY")
949
+ print(f"{'=' * 70}")
950
+ for name, path in paths.items():
951
+ if path and os.path.exists(path):
952
+ ds = load_from_disk(path)
953
+ print(f" {name:15s}: {len(ds):6d} pairs [{path}]")
954
+
955
+ print("\nReady for Stage 2 multi-expert training.")
956
+ print("Done.")
957
+
958
+
959
+ if __name__ == "__main__":
960
+ main()