changh95's picture
Add Tenstorrent Blackhole tt-nn port
ba7051a verified
Raw History Blame Contribute Delete
10.5 kB
import numpy as np
from numpy import ndarray
from typing import *
from numbers import Number, Integral
import warnings
import functools
from .utils import reverse_permutation
__all__ = [
'segment_roll',
'segment_take',
'segment_argmax',
'segment_argmin',
'segment_concatenate',
'segment_concat',
'segment_chain',
'group_as_segments'
]
def segment_roll(data: ndarray, offsets: ndarray, shift: int, axis: int = 0) -> ndarray:
"""Roll the data within each segment.
Parameters
------
- `data`: (ndarray).
- `offsets`: (ndarray) shape `(M + 1,)` the offsets of the segmented data. `M` is the number of segments. Starts with 0 and end with `data.shape[axis]`.
- `shift`: (int) the number of places by which elements are shifted. If negative, shift to left.
- `axis`: (int) the segment axis to roll along. Default is 0.
Returns
-------
- `data`: (ndarray) the rolled data, same shape as input.
"""
lengths = np.diff(offsets)
start = np.repeat(offsets[:-1], lengths)
elem_indices = start + (np.arange(data.shape[axis], dtype=offsets.dtype) - start - shift) % np.repeat(lengths, lengths)
data = np.take(data, elem_indices, axis=axis)
return data
def segment_take(data: ndarray, offsets: ndarray, taking: ndarray, axis: int = 0) -> Tuple[ndarray, ndarray]:
"""Take some segments from a segmented array
Parameters
------
- `data`: (ndarray) the segmented data.
- `offsets`: (ndarray) shape `(M + 1,)` the offsets of the segmented data. `M` is the number of segments. Starts with 0 and end with `data.shape[axis]`.
- `taking`: (ndarray) the indices of segments to take of shape `(K,)`, or boolean mask of shape `(M,)`
- `axis`: (int) the segment axis to take along. Default is 0. Other axes are treated as batch dimensions.
Returns
-------
- `new_data`: (ndarray) the new segmented data.
- `new_offsets`: (ndarray) shape `(K + 1,)` the offsets of the new segmented data. `K` is the number of taken segments.
"""
if taking.dtype == np.bool_:
taking = np.where(taking)[0]
lengths = np.diff(offsets)
new_lengths = lengths[taking]
new_offsets = np.concatenate([[0], np.cumsum(new_lengths)])
indices = np.arange(new_offsets[-1]) + np.repeat(offsets[taking] - new_offsets[:-1], new_lengths)
new_data = np.take(data, indices, axis=axis)
return new_data, new_offsets
def segment_concatenate(segments: Sequence[Tuple[ndarray, ndarray]], axis: int = 0) -> Tuple[ndarray, ndarray]:
"""Concatenate segmented arrays within each segment. All numbers of segments remain the same.
Parameters
------
- `segments`: (Sequence[Tuple[ndarray, ndarray]]) A sequence of segmented arrays:
- `data`: (ndarray) shape `(..., N_i, ...)`
- `offsets`: (ndarray) shape `(M + 1,)` segment offsets.
- `axis`: (int) the segment axis.
Returns
-------
- `data`: (ndarray) shape `(..., sum(N_i), ...)` the concatenated data
- `offsets`: (ndarray) shape `(M + 1,)` the offsets of the concatenated segmented data.
"""
if len(segments) == 0:
return np.array([]), np.array([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 = np.concatenate(data_list, axis=axis) # (..., sum(N_i), ...)
lengths_concat_0 = np.diff(np.stack(offsets_list, axis=0), axis=1).reshape(-1) # (K, M)
lengths_concat_1 = lengths_concat_0.reshape(K, M).swapaxes(0, 1).reshape(-1) # (M, K)
offsets_concat_0 = np.cumsum(lengths_concat_0.reshape(-1), axis=0) # (K * M,)
offsets_concat_1 = np.cumsum(lengths_concat_1.reshape(-1), axis=0) # (M * K,)
indices = np.arange(input_data.shape[0]) + np.repeat(offsets_concat_0.reshape(K, M).swapaxes(0, 1).reshape(-1) - offsets_concat_1, lengths_concat_1.reshape(-1))
new_data = np.take(input_data, indices, axis=axis)
new_lengths = np.sum(lengths_concat_0.reshape(K, M), axis=0)
new_offsets = np.concatenate([[0], np.cumsum(new_lengths)])
return new_data, new_offsets
def segment_concat(segments: Sequence[Tuple[ndarray, ndarray]], axis: int = 0) -> Tuple[ndarray, ndarray]:
"""(Alias for segment_concatenate).
Concatenate segmented arrays within each segment.
Parameters
------
- `segments`: (Sequence[Tuple[ndarray, ndarray]]) A sequence of segmented arrays:
- `data`: (ndarray) shape `(..., N_i, ...)`
- `offsets`: (ndarray) shape `(M + 1,)` segment offsets.
- `axis`: (int) the segment axis.
Returns
-------
- `data`: (ndarray) shape `(N, *data_dims)` the concatenated data
- `offsets`: (ndarray) shape `(M + 1,)` the offsets of the concatenated segmented data.
"""
return segment_concatenate(segments, axis=axis)
def segment_chain(segments: Sequence[Tuple[ndarray, ndarray]], axis: int = 0) -> Tuple[ndarray, ndarray]:
"""Concatenate segmented arrays in sequence. The number of segments are summed.
Parameters
------
- `segments`: (Sequence[Tuple[ndarray, ndarray]]) A sequence of segmente arrays:
- `data`: (ndarray) shape `(..., N_i, ...)`
- `offsets`: (ndarray) shape `(M + 1,)` segment offsets.
- `axis`: (int) the segment axis.
Returns
-------
- `data`: (ndarray) shape `(..., sum(N_i), ...)` the chain-concatenated data
- `offsets`: (ndarray) shape `(sum(M_i) + 1,)` the offsets of the concatenated segmented data.
"""
data_list = []
offsets_list = [np.array([0])]
for data, offsets in segments:
if len(offsets) > 1:
data_list.append(data)
offsets_list.append(offsets[1:] + offsets_list[-1][-1])
new_data = np.concatenate(data_list, axis=axis)
new_offsets = np.concatenate(offsets_list, axis=0)
return new_data, new_offsets
def group_as_segments(labels: ndarray, data: Optional[np.ndarray] = None, return_inverse: bool = False, return_group_ids: bool = False) -> Tuple[ndarray, ndarray, ndarray]:
"""
Group as segments by labels
Parameters
-----
- `labels` (ndarray): shape `(N, *label_dims)` array of labels for each data point. Labels can be multi-dimensional.
- `data` (ndarray, optional): shape `(N, *data_dims)` array.
If None, return the indices in each group instead.
Returns
-------
Assuming there are `M` difference labels:
- `segment_labels`: `(ndarray)` shape `(M, *label_dims)` labels of of each segment
- `rearranged_data`: `(ndarray)` shape `(N,)` or `(N, *data_dims)` the rearranged data (or indices) where the same labels are grouped as a continous segment.
- `offsets`: `(ndarray)` shape `(M + 1,)`
`rearranged_data[offsets[i]:offsets[i + 1]]` corresponding to the i-th segment whose label is `segment_labels[i]`
"""
group_labels, group_ids, counts = np.unique(labels, return_inverse=True, return_counts=True, axis=0)
offsets = np.concatenate([[0], np.cumsum(counts, axis=0)])
indices = np.argsort(group_ids)
if data is None:
data = indices
else:
data = data[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 group_labels, data, offsets
def segment_argmax(data: ndarray, offsets: ndarray, axis: int = 0) -> ndarray:
"""Compute the argmax of each segment in the segmented data.
Parameters
-----
- `data`: (ndarray). shape `(..., N, ...)`. `N` is the segment dimension. If `data` may have multiple dimensionsm, extra dimensions are treated as batch dimensions.
- `offsets`: (ndarray) shape `(M + 1,)` the offsets of the segmented data
- `axis`: (int) the segment axis to compute along. Default is 0.
Returns
-------
- `argmax_indices`: (ndarray) shape `(..., M, ...)` the argmax indices of each segment along the first dimension.
Notes
-----
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.
"""
axis = axis % data.ndim
lengths = np.diff(offsets)
seg_maxs = np.maximum.reduceat(data, offsets[:-1], axis=axis)
seg_ids = np.repeat(np.arange(len(offsets) - 1), lengths)
where_in_data = np.where(data == seg_maxs.take(seg_ids, axis=axis))
where_in_argmax = (*where_in_data[:axis], seg_ids[where_in_data[axis]], *where_in_data[axis + 1:])
value_in_argmax = where_in_data[axis]
argmax = np.full(data.shape[:axis] + (len(offsets) - 1,) + data.shape[axis + 1:], fill_value=np.iinfo(np.int64).max, dtype=np.int64)
np.minimum.at(argmax, where_in_argmax, value_in_argmax)
argmax[argmax == np.iinfo(np.int64).max] = -1
return argmax
def segment_argmin(data: ndarray, offsets: ndarray, axis: int = 0) -> ndarray:
"""Compute the argmin of each segment in the segmented data.
Parameters
-----
- `data`: (ndarray) shape `(..., N, ...)` the data to compute argmin from. If `data` may have multiple dimensionsm, extra dimensions are treated as batch dimensions.
- `offsets`: (ndarray) shape `(M + 1,)` the offsets of the segmented data
- `axis`: (int) the segment axis to compute along. Default is 0.
Returns
-----
- `argmin_indices`: (ndarray) shape `(..., M, ...)` the argmin indices of each segment along the first dimension.
Notes
-----
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.
"""
axis = axis % data.ndim
lengths = np.diff(offsets)
seg_mins = np.minimum.reduceat(data, offsets[:-1], axis=axis)
seg_ids = np.repeat(np.arange(len(offsets) - 1), lengths)
where_in_data = np.where(data == seg_mins.take(seg_ids, axis=axis))
where_in_argmin = (*where_in_data[:axis], seg_ids[where_in_data[axis]], *where_in_data[axis + 1:])
value_in_argmin = where_in_data[axis]
argmin = np.full(data.shape[:axis] + (len(offsets) - 1,) + data.shape[axis + 1:], fill_value=np.iinfo(np.int64).max, dtype=np.int64)
np.minimum.at(argmin, where_in_argmin, value_in_argmin)
argmin[argmin == np.iinfo(np.int64).max] = -1
return argmin