File size: 31,315 Bytes
ba7051a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 | 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
|