Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / data / megatron / blendable_dataset.py: 22%
45 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-13 05:07 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-13 05:07 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Blend multiple datasets with per-source weights.
17Given ``N`` sub-datasets and ``N`` non-negative weights ``w_i``, a sample at
18global index ``g`` is read from sub-dataset ``dataset_index[g]`` at local
19index ``dataset_sample_index[g]``. Both indices are precomputed once (and
20kept in memory) using a deterministic deficit scheduler, so the same
21``(weights, num_samples)`` tuple always yields the same blend. ``seed`` is
22accepted only for API compatibility.
24Use this to mix corpora — e.g. 30 % code, 70 % web — while keeping the
25downstream :class:`GPTDataset` API intact (each sub-dataset can be a
26GPTDataset itself, so blending stacks cleanly).
27"""
28import logging
29from typing import Any, Dict, List, Sequence
31import numpy as np
32from torch.utils.data import Dataset
35logger = logging.getLogger(__name__)
38def _build_blend_indices(
39 dataset_sizes: Sequence[int],
40 weights: Sequence[float],
41 num_samples: int,
42 seed: int, # pylint: disable=W0613
43) -> tuple:
44 """Return ``(dataset_index, dataset_sample_index)`` for a weighted blend.
46 At every global slot ``g``, pick the source whose realised fraction lags
47 its target the most (i.e. ``target * (g+1) - realised`` is maximal).
48 This guarantees the realised blend tracks ``weights`` exactly up to a
49 one-sample rounding gap, with no run-to-run variance.
51 ``seed`` is accepted for API parity but unused; the algorithm is
52 deterministic in ``(weights, num_samples)``.
53 """
54 if len(dataset_sizes) != len(weights):
55 raise ValueError(
56 f"dataset_sizes ({len(dataset_sizes)}) and weights ({len(weights)}) "
57 f"must have the same length"
58 )
59 if any(w < 0 for w in weights):
60 raise ValueError(f"weights must be non-negative, got {list(weights)}")
61 total_w = float(sum(weights))
62 if total_w <= 0:
63 raise ValueError("weights must contain at least one non-zero value")
64 norm_w = np.asarray([w / total_w for w in weights], dtype=np.float64)
65 sizes = np.asarray([int(s) for s in dataset_sizes], dtype=np.int64)
66 # A zero-length source with a non-zero weight would be selected by the
67 # greatest-error rule and then hit ``counters[d] % sizes[d]`` → divide by
68 # zero. Reject up-front (a source contributing samples must have samples).
69 empty_weighted = [i for i, (s, w) in enumerate(zip(sizes, weights)) if s == 0 and w > 0]
70 if empty_weighted:
71 raise ValueError(
72 f"BlendableDataset sources {empty_weighted} have a non-zero weight "
73 f"but zero length; an empty source cannot contribute samples"
74 )
76 dataset_index = np.zeros(num_samples, dtype=np.int32)
77 dataset_sample_index = np.zeros(num_samples, dtype=np.int64)
78 realised = np.zeros(len(weights), dtype=np.float64)
79 counters = np.zeros(len(weights), dtype=np.int64)
80 for g in range(num_samples):
81 # error[d] = how far behind source d's realised fraction is —
82 # picking the argmax keeps every source within 1 sample of its
83 # target share at every prefix (Megatron's invariant).
84 error = norm_w * (g + 1) - realised
85 d = int(np.argmax(error))
86 dataset_index[g] = d
87 # Modulo wrap so a small sub-dataset reused inside a long blend
88 # cycles through its samples in order instead of indexing OOB.
89 dataset_sample_index[g] = counters[d] % sizes[d]
90 realised[d] += 1.0
91 counters[d] += 1
92 return dataset_index, dataset_sample_index
95class BlendableDataset(Dataset):
96 """Weighted blend over a list of datasets.
98 Args:
99 datasets: Concrete ``torch.utils.data.Dataset`` objects; each must
100 implement ``__len__`` and ``__getitem__``. Typically these are
101 :class:`GPTDataset` instances.
102 weights: One non-negative weight per dataset (auto-normalised).
103 num_samples: Total length exposed by the blend.
104 seed: Accepted for API compatibility; index construction is
105 deterministic and does not use random sampling.
107 Note:
108 At every prefix, each source's realised count tracks its weighted
109 target within the scheduler's one-sample rounding bound.
110 """
112 def __init__(
113 self,
114 datasets: List[Dataset],
115 weights: Sequence[float],
116 num_samples: int,
117 seed: int = 1234,
118 ) -> None:
119 if not datasets:
120 raise ValueError("BlendableDataset requires at least one dataset")
121 self.datasets = datasets
122 sizes = [len(d) for d in datasets]
123 self.num_samples = int(num_samples)
124 self.dataset_index, self.dataset_sample_index = _build_blend_indices(
125 sizes, weights, self.num_samples, seed,
126 )
127 logger.info(
128 "BlendableDataset built: %d sub-datasets, weights=%s, total samples=%d",
129 len(datasets), list(weights), self.num_samples,
130 )
132 def __len__(self) -> int:
133 return self.num_samples
135 def __getitem__(self, idx: int) -> Dict[str, Any]:
136 d = int(self.dataset_index[idx])
137 s = int(self.dataset_sample_index[idx])
138 return self.datasets[d][s]