| """Shard reader for EDT — reads .npy shards directly, no fusion needed. |
| |
| This replaces the old `torch.load('corpus.pt')` which crashed on files >50GB. |
| The training reads shards one at a time via mmap (zero RAM cost). |
| """ |
| import os, glob, random |
| import numpy as np |
| import torch |
|
|
|
|
| class ShardCorpusReader: |
| """Read tokenized corpus from .npy shards without loading everything in RAM. |
| |
| Usage: |
| reader = ShardCorpusReader('data/fractus_1b_shards/') |
| tokens = reader.get_random_batch(batch_size=8, seq_len=32, device='cuda') |
| # tokens: (batch_size, seq_len) long tensor |
| """ |
|
|
| def __init__(self, shard_dir: str): |
| self.shards = sorted(glob.glob(os.path.join(shard_dir, "*_*.npy"))) |
| if not self.shards: |
| raise FileNotFoundError(f"No shards found in {shard_dir}") |
|
|
| |
| self.shard_lengths = [] |
| for s in self.shards: |
| arr = np.load(s, mmap_mode="r") |
| self.shard_lengths.append(len(arr)) |
|
|
| self.total_tokens = sum(self.shard_lengths) |
| print(f" ShardCorpusReader: {len(self.shards)} shards, " |
| f"{self.total_tokens/1e9:.2f}B tokens", flush=True) |
|
|
| def get_random_batch(self, batch_size: int, seq_len: int, device="cpu"): |
| """Get a random batch of token sequences from the shards. |
| |
| Picks random positions across all shards, reads seq_len tokens from each. |
| """ |
| batch_tokens = [] |
| for _ in range(batch_size): |
| |
| shard_idx = random.choices( |
| range(len(self.shards)), |
| weights=self.shard_lengths |
| )[0] |
|
|
| shard = np.load(self.shards[shard_idx], mmap_mode="r") |
| shard_len = self.shard_lengths[shard_idx] |
|
|
| |
| if shard_len <= seq_len + 1: |
| continue |
| start = random.randint(0, shard_len - seq_len - 1) |
|
|
| |
| tokens = shard[start:start + seq_len + 1] |
| batch_tokens.append(tokens) |
|
|
| if not batch_tokens: |
| |
| shard = np.load(self.shards[0], mmap_mode="r") |
| tokens = shard[:seq_len + 1] |
| batch_tokens = [tokens] * batch_size |
|
|
| |
| data = np.stack(batch_tokens) |
| data_tensor = torch.from_numpy(data.astype(np.int64)).to(device) |
|
|
| inp = data_tensor[:, :-1] |
| tgt = data_tensor[:, 1:] |
|
|
| return inp, tgt |
|
|
| def get_random_tokens(self, n: int, device="cpu"): |
| """Get n random token IDs from the corpus (for embedding training).""" |
| tokens = [] |
| for _ in range(n): |
| shard_idx = random.choices( |
| range(len(self.shards)), |
| weights=self.shard_lengths |
| )[0] |
| shard = np.load(self.shards[shard_idx], mmap_mode="r") |
| pos = random.randint(0, self.shard_lengths[shard_idx] - 1) |
| tokens.append(shard[pos]) |
|
|
| return torch.tensor(tokens, dtype=torch.long, device=device) |
|
|
| def stream_tokens(self, batch_size=128, seq_len=64, device="cpu"): |
| """Generator that yields batches sequentially through the corpus.""" |
| for shard_path in self.shards: |
| shard = np.load(shard_path, mmap_mode="r") |
| shard_len = len(shard) |
|
|
| for start in range(0, shard_len - seq_len - 1, seq_len * batch_size): |
| batch_tokens = [] |
| for b in range(batch_size): |
| pos = start + b * seq_len |
| if pos + seq_len + 1 >= shard_len: |
| break |
| batch_tokens.append(shard[pos:pos + seq_len + 1]) |
|
|
| if len(batch_tokens) < batch_size: |
| break |
|
|
| data = np.stack(batch_tokens) |
| data_tensor = torch.from_numpy(data.astype(np.int64)).to(device) |
| yield data_tensor[:, :-1], data_tensor[:, 1:] |
|
|