-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Vectorize interleave_datasets index generation (probabilities + first/all_exhausted) #8318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7210,16 +7210,64 @@ def _interleave_map_style_datasets( | ||||||||||
| # We have to keep the indices to their respective dataset offsets and to flatten to effectively interleave the datasets | |||||||||||
| indices = (indices + offsets).flatten().tolist() | |||||||||||
|
|
|||||||||||
| elif stopping_strategy in ("first_exhausted", "all_exhausted"): | |||||||||||
| # Vectorized equivalent of the per-example Python loop below (kept for | |||||||||||
| # `all_exhausted_without_replacement`). For `first_exhausted` / | |||||||||||
| # `all_exhausted` the loop just appends, for each randomly drawn source, | |||||||||||
| # that source's next index with wrap-around on exhaustion (rolling | |||||||||||
| # window), and stops when the first (any) / every (all) source has been | |||||||||||
| # drawn at least `length` times. That is fully vectorizable, giving a | |||||||||||
| # large speedup for big outputs (e.g. ~90 min -> ~5 s for a ~93M-row | |||||||||||
| # interleave) while producing bit-identical output for a fixed `seed`, | |||||||||||
| # since we consume the RNG in the exact same 1000-sized `choice` blocks. | |||||||||||
| lengths_arr = np.asarray(lengths, dtype=np.int64) | |||||||||||
| n_datasets = len(lengths) | |||||||||||
| rng = np.random.default_rng(seed) | |||||||||||
|
|
|||||||||||
| # Draw source indices in 1000-sized blocks (matching the original | |||||||||||
| # iter_random_indices) until the stopping condition can be evaluated, | |||||||||||
| # i.e. until enough sources have reached their length. | |||||||||||
| blocks = [] | |||||||||||
| counts = np.zeros(n_datasets, dtype=np.int64) | |||||||||||
| reached = np.zeros(n_datasets, dtype=bool) | |||||||||||
| while not (reached.any() if not oversampling else reached.all()): | |||||||||||
| block = rng.choice(n_datasets, size=1000, p=probabilities) | |||||||||||
| blocks.append(block) | |||||||||||
| counts += np.bincount(block, minlength=n_datasets) | |||||||||||
| reached = counts >= lengths_arr | |||||||||||
| draws = np.concatenate(blocks) | |||||||||||
|
|
|||||||||||
| # For each source, the draw-position of its `length`-th occurrence (when | |||||||||||
| # it becomes exhausted). The original loop checks the stop condition | |||||||||||
| # BEFORE appending, so the last kept draw is at: | |||||||||||
| # - first_exhausted: the earliest such position (min over sources that | |||||||||||
| # reached length) minus... no: stop fires as soon as ANY source is | |||||||||||
| # exhausted, which happens right AFTER that source's length-th draw. | |||||||||||
| # So we keep draws up to and including that length-th occurrence. | |||||||||||
| # - all_exhausted: the latest such position (max), inclusive. | |||||||||||
|
saforem2 marked this conversation as resolved.
Outdated
|
|||||||||||
| exhaust_pos = np.full(n_datasets, -1, dtype=np.int64) | |||||||||||
| for s in range(n_datasets): | |||||||||||
| occ = np.flatnonzero(draws == s) | |||||||||||
| if len(occ) >= lengths[s]: | |||||||||||
| exhaust_pos[s] = occ[lengths[s] - 1] | |||||||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I benchmarked the argsort-based single-pass approach against the current per-source At 93M draws (dummy lengths, index-mapping section only):
The
|
|||||||||||
| valid_pos = exhaust_pos[exhaust_pos >= 0] | |||||||||||
| stop_at = valid_pos.min() if not oversampling else valid_pos.max() | |||||||||||
| used = draws[: stop_at + 1] | |||||||||||
|
|
|||||||||||
| # Map each source's k-th appearance to its k-th index with wrap-around: | |||||||||||
| # concatenated row = (k % length) + offset. | |||||||||||
| indices_arr = np.empty(len(used), dtype=np.int64) | |||||||||||
| for s in range(n_datasets): | |||||||||||
| pos = np.flatnonzero(used == s) | |||||||||||
| indices_arr[pos] = (np.arange(len(pos)) % lengths[s]) + offsets[s] | |||||||||||
| indices = indices_arr.tolist() | |||||||||||
|
|
|||||||||||
| else: | |||||||||||
| # boolean array indicating if at index i if the dataset_i has been fully exhausted | |||||||||||
| # all_exhausted_without_replacement: each sample appears exactly once, so | |||||||||||
| # an exhausted source is skipped rather than wrapped -- the output length | |||||||||||
| # is not simply the draw count, so keep the explicit per-example loop. | |||||||||||
| is_exhausted = np.full(len(lengths), False) | |||||||||||
|
|
|||||||||||
| # if undersampling ("first_exhausted"), we stop as soon as one dataset is exhausted | |||||||||||
| # if oversampling ("all_exhausted"), we stop as soons as every dataset is exhausted, i.e as soon as every samples of every dataset has been visited at least once | |||||||||||
| bool_strategy_func = ( | |||||||||||
| np.all if (oversampling or stopping_strategy == "all_exhausted_without_replacement") else np.any | |||||||||||
| ) | |||||||||||
|
|
|||||||||||
| def iter_random_indices(): | |||||||||||
| """Get an infinite iterator that randomly samples the index of the source to pick examples from.""" | |||||||||||
| rng = np.random.default_rng(seed) | |||||||||||
|
|
@@ -7229,24 +7277,19 @@ def iter_random_indices(): | ||||||||||
| current_index = [0] * len(datasets) | |||||||||||
| indices = [] | |||||||||||
| for source_idx in iter_random_indices(): | |||||||||||
| # If no oversampling, we stop as soon as a dataset has ran out of examples (np.any) | |||||||||||
| # Otherwise, we stop as soon as every dataset has ran out of examples (np.all) | |||||||||||
| if bool_strategy_func(is_exhausted): | |||||||||||
| # the stopping condition was reached, let's stop | |||||||||||
| # Stop as soon as every dataset has run out of examples (np.all). | |||||||||||
| if np.all(is_exhausted): | |||||||||||
| break | |||||||||||
|
|
|||||||||||
| # let's add the example at the current index of the `source_idx`-th dataset | |||||||||||
| # For without replacement sampling we additionally need to make sure the current source is not exhausted to not oversample. | |||||||||||
| if stopping_strategy != "all_exhausted_without_replacement" or not is_exhausted[source_idx]: | |||||||||||
| # Add the example at the current index of the source_idx-th dataset, | |||||||||||
| # unless that source is already exhausted (no oversampling). | |||||||||||
| if not is_exhausted[source_idx]: | |||||||||||
| indices.append(current_index[source_idx] + offsets[source_idx]) | |||||||||||
| current_index[source_idx] += 1 | |||||||||||
|
|
|||||||||||
| # we've ran out of examples for the current dataset, let's update our boolean array and bring the current_index back to 0 | |||||||||||
| # Mark exhaustion; do not reset the index (no replacement). | |||||||||||
| if current_index[source_idx] >= lengths[source_idx]: | |||||||||||
| is_exhausted[source_idx] = True | |||||||||||
| # We don't want to reset the iterator when stopping strategy is without replacement. | |||||||||||
| if stopping_strategy != "all_exhausted_without_replacement": | |||||||||||
| current_index[source_idx] = 0 | |||||||||||
|
|
|||||||||||
| return concatenated_datasets.select(indices, **kwargs) | |||||||||||
|
|
|||||||||||
|
|
|||||||||||
Uh oh!
There was an error while loading. Please reload this page.