Download source/src/vimeml/posttraining/streams.py from Voltline/vimeml-tiny-ja-v4.2: direct link, hf CLI and curl.
- Browser
- Download file 6.55 kB
-
https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/src/vimeml/posttraining/streams.py
- Command line
-
hf download hf://Voltline/vimeml-tiny-ja-v4.2/source/src/vimeml/posttraining/streams.py
-
curl -L -o streams.py https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/src/vimeml/posttraining/streams.py
6.55 kB
| """Bucketed task streams with committed cursors, independent of worker prefetch.""" | |
| import math | |
| import numpy as np | |
| import torch | |
| from torch.utils.data import DataLoader | |
| from vimeml.training.data import BlockShuffleSampler, LengthBucketBatchSampler | |
| from vimeml.training.train import worker_init | |
| class ConversionWindows: | |
| """Keep the frozen conversion reader unchanged, including its source hash.""" | |
| context_length = 128 | |
| epoch_indexed = False | |
| def __init__(self, data): | |
| self.data, self.split = data, data.split | |
| def __len__(self): | |
| return len(self.data) | |
| def __getitem__(self, index): | |
| return self.data[int(index)] | |
| def window_lengths(self, indices): | |
| indices = np.asarray(indices, dtype=np.int64) | |
| # Normal-reading length is a cheap upper bound for the fixed10% empty | |
| # reading views; actual padding/denominators always use returned rows. | |
| return (self.data.offsets[indices + 1] - self.data.offsets[indices] - 1).astype(np.int64) | |
| class TaskCollator: | |
| def __call__(self, samples): | |
| lengths = torch.tensor([len(s["input_ids"]) for s in samples], dtype=torch.long) | |
| width = math.ceil(int(lengths.max()) / 8) * 8 | |
| if not 1 <= width <= 128: | |
| raise ValueError("Task sample outside the fixed context") | |
| ids = torch.zeros((len(samples), width), dtype=torch.long) | |
| labels = torch.full_like(ids, -100) | |
| for row, (sample, n) in enumerate(zip(samples, lengths.tolist())): | |
| if len(sample["labels"]) != n: | |
| raise ValueError("Task labels must already be shifted") | |
| ids[row, :n] = torch.tensor(sample["input_ids"], dtype=torch.long) | |
| labels[row, :n] = torch.tensor(sample["labels"], dtype=torch.long) | |
| return {"input_ids": ids, "labels": labels, "lengths": lengths} | |
| class TailLastBucketBatchSampler(LengthBucketBatchSampler): | |
| """Keep the single partial batch last, including after cursor restoration. | |
| Base LM batching shuffles the tail inside its last pool. C1's exact update | |
| accounting requires all earlier conversion updates to contain256 examples. | |
| """ | |
| def __iter__(self): | |
| base = LengthBucketBatchSampler(self.dataset, self.sampler, self.batch_size, | |
| self.multiplier, self.seed, start_batch=0) | |
| base.set_epoch(self.epoch, 0) | |
| cursor, tail = 0, None | |
| for batch in base: | |
| if len(batch) < self.batch_size: | |
| if tail is not None: | |
| raise ValueError("More than one partial task batch") | |
| tail = batch | |
| else: | |
| if cursor >= self.start_batch: | |
| yield batch | |
| cursor += 1 | |
| if tail is not None and cursor >= self.start_batch: | |
| yield tail | |
| def loader_for(data, batch_size, workers, seed, multiplier=64, indices=None, shuffle=True, pin_memory=False): | |
| sampler = (BlockShuffleSampler(len(data), seed) if shuffle else range(len(data))) if indices is None else indices | |
| batches = TailLastBucketBatchSampler(data, sampler, batch_size, multiplier, seed) | |
| options = {"multiprocessing_context": "spawn", "prefetch_factor": 2} if workers else {} | |
| return DataLoader(data, batch_sampler=batches, num_workers=workers, | |
| collate_fn=TaskCollator(), pin_memory=pin_memory, | |
| persistent_workers=workers > 0, worker_init_fn=worker_init, | |
| generator=torch.Generator().manual_seed(seed), **options) | |
| def slice_batch(batch, start, end): | |
| return {name: value[start:end] for name, value in batch.items()} | |
| class CommittedStream: | |
| """The state always points to the next unconsumed row in a bucketed batch. | |
| Caller copies state only AFTER a successful optimizer update. A partial LM | |
| tail borrows rows from the next LM epoch without dropping/repeating data. | |
| """ | |
| def __init__(self, loader, state=None): | |
| self.loader = loader | |
| self.epoch, self.batch, self.offset = (0, 0, 0) if state is None else ( | |
| state["epoch"], state["batch"], state["offset"]) | |
| if any(type(x) is not int or x < 0 for x in (self.epoch, self.batch, self.offset)): | |
| raise ValueError("Invalid task cursor") | |
| self.iterator = self.pending = None | |
| self.loader.batch_sampler.set_epoch(self.epoch, self.batch) | |
| def state_dict(self): | |
| return {"epoch": self.epoch, "batch": self.batch, "offset": self.offset} | |
| def next_batch(self): | |
| if self.offset: | |
| raise ValueError("Whole-batch consumption requires an aligned cursor") | |
| if self.iterator is None: | |
| self.iterator = iter(self.loader) | |
| value = next(self.iterator) | |
| self.batch += 1 | |
| return value | |
| def next_group(self, size): | |
| result, remaining = [], size | |
| while remaining: | |
| if self.pending is None: | |
| if self.iterator is None: | |
| self.iterator = iter(self.loader) | |
| try: | |
| self.pending = next(self.iterator) | |
| except StopIteration: | |
| if self.batch == 0: | |
| raise ValueError("Empty replay stream") | |
| self.epoch, self.batch, self.offset = self.epoch + 1, 0, 0 | |
| self.loader.batch_sampler.set_epoch(self.epoch, 0) | |
| self.iterator = iter(self.loader) | |
| self.pending = next(self.iterator) | |
| available = len(self.pending["lengths"]) - self.offset | |
| if available <= 0: | |
| raise ValueError("Replay cursor outside its saved batch") | |
| take = min(available, remaining) | |
| result.append(slice_batch(self.pending, self.offset, self.offset + take)) | |
| remaining -= take | |
| self.offset += take | |
| if self.offset == len(self.pending["lengths"]): | |
| self.offset, self.pending = 0, None | |
| self.batch += 1 | |
| return result | |
| def microbatches(batches, size): | |
| for batch in batches: | |
| for start in range(0, len(batch["lengths"]), size): | |
| part = slice_batch(batch, start, start + size) | |
| width = math.ceil(int(part["lengths"].max()) / 8) * 8 | |
| # Slicing the time dimension retains pinned storage. .to() handles | |
| # the strided source; never pad a short microbatch to another's max. | |
| part["input_ids"] = part["input_ids"][:, :width] | |
| part["labels"] = part["labels"][:, :width] | |
| yield part | |