from typing import * from numbers import Number from itertools import chain from numbers import Integral import math import torch from torch import Tensor import torch.nn.functional as F from .utils import lexsort, index_reduce_, reverse_permutation __all__ = [ 'segment_roll', 'segment_take', 'segment_concatenate', 'segment_concat', 'segment_chain', 'segment_argmax', 'segment_argmin', 'segment_median', 'segment_sum', 'segment_cumsum', 'group_as_segments', 'segment_sort', 'segment_argsort', 'segment_topk', 'stack_segments', 'segment_multinomial', 'segment_combinations', 'segment_searchsorted' ] def _lengths_to_offsets(lengths: torch.Tensor) -> torch.Tensor: return torch.cat([torch.zeros(1, dtype=lengths.dtype, device=lengths.device), torch.cumsum(lengths, dim=0)]) def segment_roll(data: torch.Tensor, offsets: torch.Tensor, shift: int, dim: int = 0) -> Tensor: """Roll the data within each segment. Parameters ------ - `data`: (Tensor). - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the segmented data. `M` is the number of segments. Starts with 0 and end with `data.shape[dim]`. - `shift`: (int) the number of places by which elements are shifted. If negative, shift to left. - `dim`: (int) the segment dimension to roll along. Default is 0. Returns ------- - `data`: (Tensor) the rolled data, same shape as input. """ lengths = torch.diff(offsets) start = offsets[:-1].repeat_interleave(lengths) elem_indices = start + (torch.arange(data.shape[dim], dtype=offsets.dtype) - start - shift) % lengths.repeat_interleave(lengths) data = data.index_select(dim, elem_indices) return data def segment_take(data: Tensor, offsets: Tensor, taking: Tensor, dim: int = 0) -> Tuple[Tensor, Tensor]: """Take some segments from a segmented array Parameters ------ - `data`: (Tensor) the segmented data. - `offsets`: (Tensor) 1-D tensor of shape `(M + 1,)` the offsets of the segmented data. `M` is the number of segments. Starts with 0 and end with `data.shape[dim]`. - `taking`: (Tensor) 1-D tensor of the indices of segments to take of shape `(K,)`, or boolean mask of shape `(M,)` - `dim`: (int) the segment dimension to take along. Default is 0. Other dimensions are treated as batch dimensions. Returns ------- - `new_data`: (Tensor) the new segmented data. - `new_offsets`: (Tensor) shape `(K + 1,)` the offsets of the new segmented data. `K` is the number of taken segments. """ if taking.dtype == torch.bool: taking = torch.where(taking)[0] lengths = torch.diff(offsets) new_lengths = lengths[taking] new_offsets = _lengths_to_offsets(new_lengths) indices = torch.arange(new_offsets[-1], device=data.device) + torch.repeat_interleave(offsets[taking] - new_offsets[:-1], new_lengths) new_data = data.index_select(dim, indices) return new_data, new_offsets def segment_concatenate(segments: Sequence[Tuple[Tensor, Tensor]], dim: int = 0) -> Tuple[Tensor, Tensor]: """Concatenate segmented arrays within each segment. All numbers of segments remain the same. Parameters ------ - `segments`: (Sequence[Tuple[Tensor, Tensor]]) A sequence of segmented arrays: - `data`: (Tensor) shape `(..., N_i, ...)` - `offsets`: (Tensor) shape `(M + 1,)` segment offsets. - `axis`: (int) the segment axis. Returns ------- - `data`: (Tensor) shape `(..., sum(N_i), ...)` the concatenated data - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the concatenated segmented data. """ if len(segments) == 0: return torch.tensor([]), torch.tensor([0]) M, K = len(segments[0][1]) - 1, len(segments) # number of segments, number of inputs assert all(M + 1 == len(seg[1]) for seg in segments), "All segments must have the same number of segments when concatenating along axis 1." data_list, offsets_list = zip(*segments) input_data = torch.cat(data_list, dim=dim) # (..., sum(N_i), ...) lengths_concat_0 = torch.diff(torch.stack(offsets_list, dim=0), dim=1).reshape(-1) # (K, M) lengths_concat_1 = lengths_concat_0.reshape(K, M).swapaxes(0, 1).reshape(-1) # (M, K) offsets_concat_0 = torch.cumsum(lengths_concat_0.reshape(-1), dim=0) # (K * M,) offsets_concat_1 = torch.cumsum(lengths_concat_1.reshape(-1), dim=0) # (M * K,) indices = torch.arange(input_data.shape[0]) + torch.repeat_interleave(offsets_concat_0.reshape(K, M).swapaxes(0, 1).reshape(-1) - offsets_concat_1, lengths_concat_1.reshape(-1)) new_data = input_data.index_select(dim, indices) new_lengths = torch.sum(lengths_concat_0.reshape(K, M), dim=0) new_offsets = _lengths_to_offsets(new_lengths) return new_data, new_offsets def segment_concat(segments: Sequence[Tuple[Tensor, Tensor]], axis: int = 0) -> Tuple[Tensor, Tensor]: """(Alias for segment_concatenate). Concatenate segmented arrays within each segment. Parameters ------ - `segments`: (Sequence[Tuple[Tensor, Tensor]]) A sequence of segmented arrays: - `data`: (Tensor) shape `(..., N_i, ...)` - `offsets`: (Tensor) shape `(M + 1,)` segment offsets. - `axis`: (int) the segment axis. Returns ------- - `data`: (Tensor) shape `(N, *data_dims)` the concatenated data - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the concatenated segmented data. """ return segment_concatenate(segments, axis=axis) def segment_chain(segments: Sequence[Tuple[Tensor, Tensor]], axis: int = 0) -> Tuple[Tensor, Tensor]: """Concatenate segmented arrays in sequence. The number of segments are summed. Parameters ------ - `segments`: (Sequence[Tuple[Tensor, Tensor]]) A sequence of segmente arrays: - `data`: (Tensor) shape `(..., N_i, ...)` - `offsets`: (Tensor) shape `(M + 1,)` segment offsets. - `axis`: (int) the segment axis. Returns ------- - `data`: (Tensor) shape `(..., sum(N_i), ...)` the chain-concatenated data - `offsets`: (Tensor) shape `(sum(M_i) + 1,)` the offsets of the concatenated segmented data. """ data_list = [] offsets_list = [torch.tensor([0], dtype=segments[0][1].dtype, device=segments[0][1].device)] for data, offsets in segments: if len(offsets) > 1: data_list.append(data) offsets_list.append(offsets[1:] + offsets_list[-1][-1]) new_data = torch.cat(data_list, dim=axis) new_offsets = torch.cat(offsets_list, dim=0) return new_data, new_offsets def group_as_segments(labels: Tensor, data: Optional[Tensor] = None, return_inverse: bool = False, return_group_ids: bool = False) -> Tuple[Tensor, ...]: """ Group as segments by labels Parameters ---- - `labels` (Tensor): shape `(N, *label_dims)` array of labels for each data point. Labels can be multi-dimensional. - `data` (Tensor, optional): shape `(N, *data_dims)` array. If None, return the indices in each group instead. Returns ------- Assuming there are `M` difference labels: - `grouped_labels`: `(Tensor)` shape `(M, *label_dims)` labels of of each segment - `grouped_data`: `(Tensor)` shape `(N,)` or `(N, *data_dims)` the rearranged data (or indices) where the same labels are grouped as a continous segment. - `offsets`: `(Tensor)` shape `(M + 1,)`. `grouped_data[offsets[i]:offsets[i + 1]]` corresponding to the i-th segment whose label is `grouped_labels[i]` - `inverse_indices` (Tensor, optional): shape `(N,)`. `data[inverse_indices]` recovers the original data order, i.e., `inverse_indices[i]` gives the position that `data[i]` goes to in the `grouped_data`. - `group_ids` (Tensor, optional): shape `(N,)`. The group id for each data point in the original order, i.e., `group_ids[i]` gives the group index that `data[i]` belongs to. """ group_labels, group_ids, counts = labels.unique(sorted=True, return_inverse=True, return_counts=True, dim=0) offsets = _lengths_to_offsets(counts) indices = torch.argsort(group_ids) if data is None: data = indices else: data = data.index_select(0, indices) ret = (group_labels, data, offsets) if return_inverse: inverse_indices = reverse_permutation(indices) ret += (inverse_indices,) if return_group_ids: ret += (group_ids,) return ret def segment_argmax(data: Tensor, offsets: Tensor, dim: int = 0) -> Tensor: """Compute the argmax of each segment in the segmented data. Parameters ---------- - `data`: (Tensor) shape `(..., N, ...)` the data to compute argmax from. If `data` may have multiple dimensions, extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the segmented data - `dim`: (int) the segment axis to compute along. Default is 0. Returns ------- - `argmax_indices`: (Tensor) shape `(..., M, ...)` the argmax indices of each segment along the first dimension. NOTE: If there are multiple maximum values in a segment, the index of the first one is returned. If a segment is empty, -1 is returned. """ dim = dim % data.ndim lengths = torch.diff(offsets) seg_maxs = torch.segment_reduce(data, 'max', offsets=offsets, axis=dim) seg_ids = torch.repeat_interleave(torch.arange(len(offsets) - 1, device=data.device), lengths) where_in_data = torch.where(data == seg_maxs.index_select(dim, seg_ids)) where_in_argmax = (*where_in_data[:dim], seg_ids[where_in_data[dim]], *where_in_data[dim + 1:]) value_in_argmax = where_in_data[dim] sentinel_value = torch.iinfo(torch.int64).max argmax = torch.full((*data.shape[:dim], len(lengths), *data.shape[dim + 1:]), fill_value=sentinel_value, dtype=torch.int64) index_reduce_(argmax, where_in_argmax, value_in_argmax, reduce='amin') argmax[argmax == sentinel_value] = -1 return argmax def segment_argmin(data: Tensor, offsets: Tensor, dim: int = 0) -> Tensor: """Compute the argmin of each segment in the segmented data. Parameters ---------- - `data`: (Tensor) shape `(..., N, ...)` the data to compute argmin from. If `data` may have multiple dimensionsm, extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the segmented data - `dim`: (int) the segment axis to compute along. Default is 0. Returns ------- - `argmin_indices`: (Tensor) shape `(..., M, ...)` the argmin indices of each segment along the first dimension. NOTE: If there are multiple minimum values in a segment, the index of the first one is returned. If a segment is empty, -1 is returned. """ dim = dim % data.ndim lengths = torch.diff(offsets) seg_mins = torch.segment_reduce(data, 'min', offsets=offsets, axis=dim) seg_ids = torch.repeat_interleave(torch.arange(len(lengths), device=data.device), lengths) where_in_data = torch.where(data == seg_mins.index_select(dim, seg_ids)) where_in_argmin = (*where_in_data[:dim], seg_ids[where_in_data[dim]], *where_in_data[dim + 1:]) value_in_argmin = where_in_data[dim] sentinel_value = torch.iinfo(torch.int64).max argmin = torch.full((*data.shape[:dim], len(lengths), *data.shape[dim + 1:]), fill_value=sentinel_value, dtype=torch.int64) index_reduce_(argmin, where_in_argmin, value_in_argmin, reduce='amin') argmin[argmin == sentinel_value] = -1 return argmin def segment_argsort(input: torch.Tensor, offsets: torch.Tensor, descending: bool = False, dim: int = -1) -> torch.Tensor: """Compute the argsort indices within each segment. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to sort. The first dimension is treated as the segment dimension. Extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of each segment. - `descending`: (bool) whether to sort in descending order. - `dim`: (int) the segment axis to sort along. Default is -1. Returns ---- - `sorted_indices`: (Tensor) shape `(..., N, ...)` the indices that would sort the data within each segment. """ dim = dim % input.ndim lengths = torch.diff(offsets) segment_ids = torch.repeat_interleave(torch.arange(len(lengths), device=input.device), lengths) segment_ids = segment_ids.reshape((1,) * dim + segment_ids.shape + (1,) * (input.ndim - 1 - dim)) sorted_indices = lexsort([-input if descending else input, segment_ids], dim=dim) return sorted_indices def segment_sort(input: torch.Tensor, offsets: torch.Tensor = None, descending: bool = False, dim: int = -1) -> torch.return_types.sort: """Sort the data within each segment. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to sort. - `lengths`: (Tensor) shape `(M,)` the lengths of each segment, alternatively to `offsets`. - `offsets`: (Tensor) shape `(M + 1,)` the offsets - `descending`: (bool) whether to sort in descending order. - `dim`: (int) the segment axis to sort along. Default is -1. Returns ---- - `sorted`: (Tensor) shape `(..., N, ...)` the sorted data. - `indices`: (Tensor) shape `(..., N, ...)` the indices that would sort the data within each segment. """ sorted_indices = segment_argsort(input, offsets, descending=descending, dim=dim) return torch.return_types.sort((input.index_select(dim, sorted_indices), sorted_indices)) def segment_topk(input: torch.Tensor, offsets: torch.Tensor, k: Union[int, torch.Tensor], largest: bool = True, dim: int = -1) -> Tuple[torch.return_types.topk, torch.Tensor]: """Select the top-k values and indices within each segment. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to compute top - `k`: (int or Tensor) the number of top elements to retrieve from each segment. If a Tensor, it should have shape `(M,)` where `M` is the number of segments. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of each segment. - `largest`: (bool) whether to return the largest or smallest elements. Otherwise, return the smallest elements. - `dim`: (int) the segment axis to compute along. Default is -1. Returns ---- - `topk`: (namedtuple) with fields `values` and `indices`. - `values`: (Tensor) shape `(sum_k, ...)` the top-k values where `sum_k` is the sum of all k's across segments. - `indices`: (Tensor) shape `(sum_k, ...)` the indices of the top-k values in the original input. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the top-k values for each segment. Notes ----- - If the length of a segment is less than k, the returns will contain all elements in that segment but fewer than k elements. """ lengths = torch.diff(offsets) sorted_indices = segment_argsort(input, offsets, descending=largest, dim=dim) local_index = torch.arange(len(input), device=input.device) - torch.repeat_interleave(offsets[:-1], lengths) if isinstance(k, int) or isinstance(k, torch.Tensor) and k.dim() == 0: topk_indices = sorted_indices.index_select(dim, local_index < k) else: topk_indices = sorted_indices.index_select(dim, local_index < torch.repeat_interleave(k, lengths)) topk_values = input.index_select(dim, topk_indices) result_offsets = _lengths_to_offsets(torch.minimum(lengths, k)) return torch.return_types.topk((topk_values, topk_indices)), result_offsets def segment_median(input: torch.Tensor, offsets: torch.Tensor, dim: int = 0) -> torch.return_types.median: """Compute the median of each segment. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to compute median from. The first dimension is treated as the segment dimension. Extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of each segment. - `dim`: (int) the segment axis to compute along. Default is 0. Returns ---- - `medians`: (Tensor) shape `(..., M, ...)` the median of each segment. - `indices`: (Tensor) shape `(..., M, ...)` the indices of the median values in the original input. Notes ----- - If a segment has even length, the lower median is returned. - If a segment is empty or has negative length, the median value is undefined. """ dim = dim % input.ndim lengths = torch.diff(offsets) sorted_indices = segment_argsort(input, offsets, descending=False, dim=dim) median_pos = offsets[:-1] + (lengths - 1) // 2 median_indices = torch.take_along_dim(sorted_indices, median_pos[(None,) * dim + (slice(None),) + (None,) * (input.ndim - dim - 1)], dim=dim) median_values = torch.take_along_dim(input, median_indices, dim=dim) return torch.return_types.median((median_values, median_indices)) def stack_segments(input: torch.Tensor, offsets: torch.Tensor, max_length: int = None, padding_value: Number = 0, dim: int = 0) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Stack segments into a padded tensor. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to stack. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of each segment. - `max_length`: (int, optional) the maximum length to pad/truncate each segment to. If None, use the maximum segment length. - `padding_value`: (Number) the value to use for padding. - `dim`: (int) the segment axis to stack along. Default is 0. Returns ---- - `stacked`: (Tensor) shape `(..., M, max_length, ...)` the stacked segments, where `max_length` is the maximum segment length. - `mask`: (Tensor) shape `(..., M, max_length)` boolean mask indicating valid entries in `stacked`. - `indices`: (Tensor) shape `(..., M, max_length)` the indices of the stacked entries in the original input. """ dim = dim % input.ndim lengths = torch.diff(offsets) if max_length is None: max_length = lengths.max().item() indices = torch.arange(0, max_length, device=input.device)[None, :] + offsets[:-1, None] mask = indices < offsets[1:, None] stacked = torch.where( mask[(None,) * (dim) + (...,) + (None,) * (input.ndim - dim - 1)], input.index_select(dim, indices.clamp(0, input.shape[dim] - 1).flatten()).reshape(input.shape[:dim] + indices.shape + input.shape[dim + 1:]), padding_value ) return stacked, mask, indices @torch.no_grad() def segment_multinomial(weights: torch.Tensor, offsets: torch.Tensor, num_samples: torch.Tensor, eps: float = 1e-12, replacement: bool = False) -> torch.LongTensor: """ Perform multinomial sampling within each segment. Parameters ---- - `weights`: (Tensor) 1-D tensor of shape `(N,)` the weights for sampling. - `offsets`: (Tensor) 1-D tensor of shape `(M + 1,)` the offsets of each segment. - `n`: (int) the number of samples to draw from each segment. - `eps`: (float) a small value to avoid division by zero. - `replacement`: (bool) whether to sample with replacement. Returns ---- - `sampled_indices`: (LongTensor) shape `(tot_samples,)` the sampled indices from each segment. - `offsets`: (LongTensor) shape `(M + 1,)` the offsets of the sampled indices for each segment. """ dtype, device = weights.dtype, weights.device lengths = torch.diff(offsets) M = len(lengths) weights_segment_sum = torch.segment_reduce(weights, 'sum', offsets=offsets) weights = weights / torch.repeat_interleave(weights_segment_sum.clamp_min(eps), lengths) if replacement: cdf = torch.cumsum(weights, dim=0) sampled_segment_ids = torch.repeat_interleave(torch.arange(M, device=device), num_samples) u = sampled_segment_ids.to(weights.dtype) + torch.rand_like(sampled_segment_ids, dtype=weights.dtype) sampled_indices = torch.searchsorted(cdf, u, right=False) sampled_indices.clamp_( min=torch.repeat_interleave(offsets[:-1], num_samples), max=torch.repeat_interleave(offsets[1:], num_samples) - 1 ) else: scores = weights.clamp_min_(eps).log() - torch.empty_like(weights).exponential_().clamp_min(eps).log() topk, offsets = segment_topk(scores, offsets, k=num_samples) return topk.indices, offsets def _segment_local_indices(lengths: torch.Tensor = None, offsets: torch.Tensor = None): if offsets is None: offsets = _lengths_to_offsets(lengths) if lengths is None: lengths = torch.diff(offsets) local_indices = torch.arange(offsets[-1].item(), device=lengths.device) - torch.repeat_interleave(offsets[:-1], lengths) return local_indices def segment_sum(input: torch.Tensor, offsets: torch.Tensor, dim: int = 0) -> torch.Tensor: """Compute the sum of each segment in the segmented data. Workaround supports for dtypes other than float32. NOTE: Silently assumes that the input does not contain negative lengths. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to compute sum from. If `input` may have multiple dimensionsm, extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the segmented data - `dim`: (int) the segment axis to compute sum along. Default is 0. Returns ---- - `segment_sums`: (Tensor) shape `(..., M, ...)` the sum of each segment along the specified dimension. """ dim = dim % input.ndim cumsum = torch.cat([ torch.zeros((*input.shape[:dim], 1, *input.shape[dim + 1:]), dtype=input.dtype, device=input.device), torch.cumsum(input, dim=dim) ], dim=dim) segsum = torch.diff(torch.index_select(cumsum, dim=dim, index=offsets), dim=dim) return segsum def segment_cumsum(input: torch.Tensor, offsets: torch.Tensor, dim: int) -> torch.Tensor: """Compute the sum of each segment in the segmented data. Workaround supports for dtypes other than float32. NOTE: Silently assumes that the input does not contain negative lengths. Parameters ---- - `input`: (Tensor) shape `(..., N, ...)` the data to compute sum from. If `input` may have multiple dimensionsm, extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of the segmented data Returns ---- - `segment_sums`: (Tensor) shape `(..., N, ...)` the cumulative sum of each segment along the specified dimension. """ dim = dim % input.ndim cumsum = torch.cat([ torch.zeros((*input.shape[:dim], 1, *input.shape[dim + 1:]), dtype=input.dtype, device=input.device), torch.cumsum(input, dim=dim) ], dim=dim) lengths = torch.diff(offsets) segcumsum = cumsum[(slice(None),) * dim + (slice(1, None),) + (slice(None),) * (input.ndim - dim - 1)]\ - cumsum.index_select(dim=dim, index=offsets[:-1]).repeat_interleave(lengths, dim=dim) return segcumsum def segment_combinations(input: torch.Tensor, offsets: torch.Tensor, r: int = 2, with_replacement: bool = False) -> torch.Tensor: """Generate all combinations of elements within each segment. Vectorized implementation. Parameters ---- - `input`: (Tensor) shape `(N,)` the data to generate combinations from. The first dimension is treated as the segment dimension. Extra dimensions are treated as batch dimensions. - `offsets`: (Tensor) shape `(M + 1,)` the offsets of each segment. - `r`: (int) the number of elements in each combination. - `with_replacement`: (bool) whether to allow repeated elements in a combination. Returns ---- - `combinations`: (Tensor) shape `(K, r,)` the combinations from all segments, where `K` is the total number of combinations across all segments. - `combination_offsets`: (Tensor) shape `(M + 1,)` the offsets of combinations for each segment. Notes ---- - The result may contain zero-length segments if a segment has less than `r` elements. """ lengths = torch.diff(offsets) if with_replacement is False: # Initialize from r=1 comb_lengths = (lengths - r + 1).clamp(min=0) comb_offsets = _lengths_to_offsets(comb_lengths) last_indices = _segment_local_indices(lengths=comb_lengths) comb_indices = last_indices[:, None] # Iteratively build up combinations (a_0, a_1, ..., a_{r-1}), for r_ in range(2, r + 1): # Extend a_k in [a_{r_-1} + 1, n - 1 - (r - r_)], of size - (r - r_ + 1) - a_{r_-1} ext_lengths = torch.repeat_interleave(lengths - (r - r_ + 1), comb_lengths) - last_indices ext_local_indices = _segment_local_indices(lengths=ext_lengths) last_indices = torch.repeat_interleave(last_indices + 1, ext_lengths) + ext_local_indices comb_indices = torch.cat([ comb_indices.repeat_interleave(ext_lengths, dim=0), last_indices[:, None] ], dim=1) # Update combination lengths comb_lengths = segment_sum(ext_lengths, comb_offsets) comb_offsets = _lengths_to_offsets(comb_lengths) comb_indices = comb_indices + torch.repeat_interleave(offsets[:-1], comb_lengths).reshape(-1, 1) combinations = input.index_select(0, comb_indices.flatten()).reshape(comb_indices.shape) else: # Expand to lengths^r comb_lengths = lengths.pow(r) comb_offsets = _lengths_to_offsets(comb_lengths) base = lengths[:, None] ** torch.arange(r - 1, -1, -1, device=lengths.device)[None, :] comb_indices = _segment_local_indices(lengths=comb_lengths)[:, None] // base.repeat_interleave(comb_lengths, dim=0) % lengths[:, None].repeat_interleave(comb_lengths, dim=0) comb_indices = comb_indices + torch.repeat_interleave(offsets[:-1], comb_lengths).reshape(-1, 1) combinations = input.index_select(0, comb_indices.flatten()).reshape(comb_indices.shape) return combinations, comb_offsets def _segment_searchsorted_pytorch( sorted_sequence: Tensor, offsets: Tensor, input: Tensor, segment_ids: Tensor, side: Literal['left', 'right'] = 'left', ) -> Tensor: """Per-segment searchsorted operation implemented with pure PyTorch. Parameters ---------- - `sorted_sequence: Tensor` of shape (..., N), the concatenated sorted sequences of all segments. - `offsets: Tensor` of shape (M + 1,), segment offsets applied to the last dimension of `sorted_sequence`. - `input: Tensor` of shape (..., Q), the values to search for. - `segment_ids: Tensor` of shape (..., Q), the segment ids to search within for each value in `input`. Returns - `indices: Tensor` of shape (..., Q), the insertion indices for each value in `input` within its corresponding segment in `sorted_sequence`. """ lengths = torch.diff(offsets) sorted_sequence_segment_ids = torch.arange(len(offsets) - 1, device=sorted_sequence.device).repeat_interleave(lengths) if side == 'left': # Combine input and sorted_sequence and lexsort (segment-wise sort) all_vals = torch.cat([input, sorted_sequence], dim=-1) all_seg_ids = torch.cat([ segment_ids, sorted_sequence_segment_ids.expand(*input.shape[:-1], -1) ], dim=-1) sorted_indices = lexsort([all_vals, all_seg_ids], dim=-1) # Indices of all_vals in sorted order sorted_ranks = reverse_permutation(sorted_indices, dim=-1) # Ranks in sorted order corresponding to all_vals # Count of sorted_sequence elements <= all_vals[..., i] count = torch.cumsum(sorted_indices >= input.shape[-1], dim=-1) # Take counts for input elements out = torch.take_along_dim(count, sorted_ranks[..., :input.shape[-1]], dim=-1) elif side == 'right': all_vals = torch.cat([sorted_sequence, input], dim=-1) all_seg_ids = torch.cat([ sorted_sequence_segment_ids.expand(*input.shape[:-1], -1), segment_ids, ], dim=-1) sorted_indices = lexsort([all_vals, all_seg_ids], dim=-1) sorted_ranks = reverse_permutation(sorted_indices, dim=-1) count = torch.cumsum(sorted_indices < sorted_sequence.shape[-1], dim=-1) out = torch.take_along_dim(count, sorted_ranks[..., sorted_sequence.shape[-1]:], dim=-1) return out def segment_searchsorted( sorted_sequence: Tensor, offsets: Tensor, input: Tensor, segment_ids: Tensor, side: Literal['left', 'right'] = 'left', ) -> Tensor: """Per-segment searchsorted operation implemented with triton. Parameters ---------- - `sorted_sequence: Tensor` of shape (..., N), the concatenated sorted sequences of all segments. - `offsets: Tensor` of shape (M + 1,), segment offsets applied to the last dimension of `sorted_sequence`. - `input: Tensor` of shape (..., Q), the values to search for. - `segment_ids: Tensor` of shape (..., Q), the segment ids to search within for each value in `input`. Returns - `indices: Tensor` of shape (..., Q), the insertion indices for each value in `input` within its corresponding segment in `sorted_sequence`. """ device_type = sorted_sequence.device.type assert side in ('left', 'right'), "side must be either 'left' or 'right'" assert input.shape == segment_ids.shape, "input and segment_ids must have the same shape" assert sorted_sequence.shape[:-1] == input.shape[:-1], "batch dimensions of sorted_sequence and input must match" if device_type == 'cpu': out = _segment_searchsorted_pytorch( sorted_sequence, offsets, input, segment_ids, side=side, ) elif device_type == 'cuda': from ._triton.searchsorted import segment_searchsorted_1d_triton # Flatten inputs, get seg_start and seg_end batch_shape = sorted_sequence.shape[:-1] batch_size = math.prod(batch_shape) N = sorted_sequence.shape[-1] input_flat = input.reshape(-1) sorted_sequence_flat = sorted_sequence.reshape(-1) seg_base = torch.arange(0, batch_size * N, N, device=sorted_sequence.device).reshape(*batch_shape, 1) seg_start = seg_base + torch.take_along_dim(offsets.expand(*batch_shape, -1), segment_ids, dim=-1) seg_end = seg_base + torch.take_along_dim(offsets.expand(*batch_shape, -1), segment_ids + 1, dim=-1) - 1 seg_start_flat = seg_start.reshape(-1) seg_end_flat = seg_end.reshape(-1) # Call triton 1D implementation max_length = torch.diff(offsets).max().item() out_flat = segment_searchsorted_1d_triton( sorted_sequence_flat, input_flat, seg_start_flat, seg_end_flat, max_length=max_length, side=side, ) out = out_flat.reshape(*batch_shape, -1) - seg_base return out