# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # pylint: disable=invalid-name, no-member, too-many-locals, too-many-arguments, too-many-statements, singleton-comparison, unused-argument, no-else-return # ruff: noqa: RUF005 """Sort related operators""" import tvm from tvm import te from tvm.script.ir_builder import IRBuilder from tvm.script.ir_builder import tirx as T from ..math import cast, ceil_log2 from ..searchsorted import binary_search from ..transform import strided_slice, transpose from ..utils import ceil_div, prod, swap def _get_threads(nthread_tx, nthread_bx, nthread_by): tx = te.thread_axis("threadIdx.x") bx = te.thread_axis("blockIdx.x") by = te.thread_axis("blockIdx.y") return tx, bx, by, nthread_tx, nthread_bx, nthread_by def _sort_init(shape, axis, keys_in, keys_out, values_out=None, value_init_func=None): """Initialize the output buffers by copying from inputs""" axis_mul_before = 1 axis_mul_after = 1 if axis < 0: axis = len(shape) + axis for i, value in enumerate(shape, 0): if i < axis: axis_mul_before *= value elif i > axis: axis_mul_after *= value # Set up threading max_threads = int(tvm.target.Target.current(allow_none=False).attrs["max_num_threads"]) nthread_tx = max_threads nthread_bx = ceil_div(shape[axis], max_threads) nthread_by = axis_mul_before * axis_mul_after # Copy the keys_in to initial output tx, bx, by, ntx, nbx, nby = _get_threads(nthread_tx, nthread_bx, nthread_by) with T.frame_scope( [ T.attr(tx, "thread_extent", ntx), T.attr(bx, "thread_extent", nbx), T.attr(by, "thread_extent", nby), ] ): tid = bx * nthread_tx + tx by_val = by % axis_mul_before bz = by // axis_mul_before idx = (by_val * shape[axis] + tid) * axis_mul_after + bz with T.If(tid < shape[axis]): with T.Then(): keys_out[idx] = keys_in[idx] if values_out is not None: values_out[idx] = value_init_func(idx, tid) return axis_mul_before, axis_mul_after ## TODO(mbrookhart): These are effective optimziation hyperparametrs ## Perhaps we can autotune? block_size = 128 thread_work = 4 def _odd_even_sort( size, axis_mul_before, axis_mul_after, is_ascend, keys, keys_swap, values=None, values_swap=None, ): nthread_tx = block_size // 2 nthread_bx = ceil_div(size, block_size) nthread_by = axis_mul_before * axis_mul_after tx, bx, by, ntx, nbx, nby = _get_threads(nthread_tx, nthread_bx, nthread_by) with T.frame_scope( [ T.attr(tvm.tirx.const(0), "hand_threaded", 0), T.attr(tx, "thread_extent", ntx), T.attr(bx, "thread_extent", nbx), T.attr(by, "thread_extent", nby), ] ): by_val = by % axis_mul_before bz = by // axis_mul_before tid = 2 * tx start = bx * block_size # Buffer declarations (DeclBuffer generates both Allocate + DeclBuffer nodes) tmp_keys_swap = T.decl_buffer([block_size], keys_swap.dtype, scope="shared") temp_keys = T.decl_buffer([1], keys_swap.dtype, scope="local") temp_cond1 = T.decl_buffer([1], keys_swap.dtype, scope="local") temp_cond2 = T.decl_buffer([1], keys_swap.dtype, scope="local") if values_swap is not None: tmp_values_swap = T.decl_buffer([block_size], values_swap.dtype, scope="shared") temp_values = T.decl_buffer([1], values_swap.dtype, scope="local") # Copy data to scratch space base_idx = by_val * size * axis_mul_after + bz with T.serial(0, 2) as n: with T.If((tid + n + start) < size): with T.Then(): T.buffer_store( tmp_keys_swap, keys[base_idx + (tid + n + start) * axis_mul_after], [tid + n], ) if values_swap is not None: T.buffer_store( tmp_values_swap, values[base_idx + (tid + n + start) * axis_mul_after], [tid + n], ) T.evaluate( tvm.ir.Call("tirx.tvm_storage_sync", [tvm.tirx.StringImm("shared")], ret_ty="void") ) idxm = tvm.tirx.indexmod # OddEvenTransposeSort current_sort_num = tvm.tirx.min(block_size, size - start) with T.serial(0, current_sort_num) as k: n = idxm(tid + k, 2) with T.If(tid + n < current_sort_num - 1): with T.Then(): T.buffer_store(temp_cond1, tmp_keys_swap[tid + n], [0]) T.buffer_store(temp_cond2, tmp_keys_swap[tid + n + 1], [0]) if is_ascend: cond = temp_cond1[0] > temp_cond2[0] else: cond = temp_cond1[0] < temp_cond2[0] with T.If(cond): with T.Then(): T.buffer_store(temp_keys, tmp_keys_swap[tid + n], [0]) T.buffer_store(tmp_keys_swap, tmp_keys_swap[tid + n + 1], [tid + n]) T.buffer_store(tmp_keys_swap, temp_keys[0], [tid + n + 1]) if values_swap is not None: T.buffer_store(temp_values, tmp_values_swap[tid + n], [0]) T.buffer_store( tmp_values_swap, tmp_values_swap[tid + n + 1], [tid + n], ) T.buffer_store(tmp_values_swap, temp_values[0], [tid + n + 1]) T.evaluate( tvm.ir.Call("tirx.tvm_storage_sync", [tvm.tirx.StringImm("shared")], ret_ty="void") ) ## Copy sorted data to output with T.serial(0, 2) as n: with T.If(tid + n + start < size): with T.Then(): out_idx = base_idx + (tid + n + start) * axis_mul_after keys[out_idx] = tmp_keys_swap[tid + n] keys_swap[out_idx] = tmp_keys_swap[tid + n] if values_swap is not None: values[out_idx] = tmp_values_swap[tid + n] values_swap[out_idx] = tmp_values_swap[tid + n] def _sort_common( size, axis_mul_before, axis_mul_after, is_ascend, keys, keys_swap, values=None, values_swap=None, ): """Either sort only values or sort values by keys.""" ## This function performs a multi-level mergesort ## For blocks of length <= block_size, it does odd-even transpose sort ## in GPU shared memory ## For intermediate block sizes (>block_size, < max_threads * thread_work) ## it uses the mergpath algorthim https://arxiv.org/abs/1406.2628 ## to merge blocks in parallel ## At some point, the size of the blocks to be merged is too big for max_threads ## and we switch to using a dual-level mergepath where the outer mergepath ## finds the start/end locations of the inner mergepath so that we can split ## the merge into more blocks target = tvm.target.Target.current(allow_none=False) max_threads = int(target.attrs["max_num_threads"]) is_webgpu = "webgpu" in str(target) target_dtype = "int32" if is_webgpu else "int64" nthread_by = axis_mul_before * axis_mul_after nthread_tx = max_threads nthread_bx = ceil_div(size, nthread_tx) def compare(a, b): """Compare a and b in proper ascending or descending order""" if is_ascend: out = a <= b else: out = b <= a return out # Sort the lower levels of the merge using odd-even sort, it's fast for small inputs lower_lim = ceil_log2(block_size) _odd_even_sort( size, axis_mul_before * axis_mul_after, 1, is_ascend, keys, keys_swap, values, values_swap, ) upper_lim = ceil_log2(size) def get_merge_begin(source, base_idx, aCount, bCount, aStart, bStart, diag, first, last): max_val = tvm.te.max(0, diag - bCount) min_val = tvm.te.min(diag, aCount) if is_webgpu: first[0] = cast(max_val, target_dtype) last[0] = cast(min_val, target_dtype) else: first[0] = max_val last[0] = min_val with T.While(first[0] < last[0]): mid = (first[0] + last[0]) >> 1 a = source[base_idx + (aStart + mid)] b = source[base_idx + (bStart + diag - 1 - mid)] with T.If(compare(a, b)): with T.Then(): first[0] = mid + 1 with T.Else(): last[0] = mid def serial_merge( source, dest, source_idx, dest_idx, base_idx, aCount, bCount, aStart, bStart, kStart, diag, step_count, first, last, i_buf, j_buf, ): i_val = aStart + first[0] j_val = bStart + diag - last[0] if is_webgpu: i_buf[0] = cast(i_val, target_dtype) j_buf[0] = cast(j_val, target_dtype) else: i_buf[0] = i_val j_buf[0] = j_val with T.serial(0, tvm.te.min(aCount + bCount - diag, step_count)) as count: i_idx = base_idx + i_buf[0] j_idx = base_idx + j_buf[0] k_idx = base_idx + (kStart + diag + count) with T.If(tvm.tirx.all(i_buf[0] < aStart + aCount, j_buf[0] < bStart + bCount)): with T.Then(): with T.If(compare(source[i_idx], source[j_idx])): with T.Then(): dest[k_idx] = source[i_idx] if values is not None: dest_idx[k_idx] = source_idx[i_idx] i_buf[0] = i_buf[0] + 1 with T.Else(): dest[k_idx] = source[j_idx] if values is not None: dest_idx[k_idx] = source_idx[j_idx] j_buf[0] = j_buf[0] + 1 with T.Else(): with T.If(i_buf[0] < aStart + aCount): with T.Then(): dest[k_idx] = source[i_idx] if values is not None: dest_idx[k_idx] = source_idx[i_idx] i_buf[0] = i_buf[0] + 1 with T.Else(): dest[k_idx] = source[j_idx] if values is not None: dest_idx[k_idx] = source_idx[j_idx] j_buf[0] = j_buf[0] + 1 def mergepath( source, dest, source_idx, dest_idx, base_idx, aCount, bCount, aStart, bStart, kStart, tx, step_count, even, ): first_buf = T.decl_buffer([1], target_dtype, scope="local") last_buf = T.decl_buffer([1], target_dtype, scope="local") i_buf_buf = T.decl_buffer([1], target_dtype, scope="local") j_buf_buf = T.decl_buffer([1], target_dtype, scope="local") first = T.buffer_proxy(first_buf) last = T.buffer_proxy(last_buf) i_buf = T.buffer_proxy(i_buf_buf) j_buf = T.buffer_proxy(j_buf_buf) diag = tx * step_count with T.If(even): with T.Then(): get_merge_begin(source, base_idx, aCount, bCount, aStart, bStart, diag, first, last) serial_merge( source, dest, source_idx, dest_idx, base_idx, aCount, bCount, aStart, bStart, kStart, diag, step_count, first, last, i_buf, j_buf, ) with T.Else(): get_merge_begin(dest, base_idx, aCount, bCount, aStart, bStart, diag, first, last) # Intentionally swap source/dest for reverse direction merge serial_merge( # pylint: disable=arguments-out-of-order dest, source, dest_idx, source_idx, base_idx, aCount, bCount, aStart, bStart, kStart, diag, step_count, first, last, i_buf, j_buf, ) def dual_mergepath( source, dest, source_idx, dest_idx, base_idx, start_pos, middle, end, bx, tx, step_count, even, ): outer_first_buf = T.decl_buffer([1], target_dtype, scope="local") outer_last_buf = T.decl_buffer([1], target_dtype, scope="local") first_buf = T.decl_buffer([1], target_dtype, scope="local") last_buf = T.decl_buffer([1], target_dtype, scope="local") i_buf_buf = T.decl_buffer([1], target_dtype, scope="local") j_buf_buf = T.decl_buffer([1], target_dtype, scope="local") outer_first = T.buffer_proxy(outer_first_buf) outer_last = T.buffer_proxy(outer_last_buf) first = T.buffer_proxy(first_buf) last = T.buffer_proxy(last_buf) i_buf = T.buffer_proxy(i_buf_buf) j_buf = T.buffer_proxy(j_buf_buf) diag = bx * step_count with T.If(even): with T.Then(): get_merge_begin( source, base_idx, middle - start_pos, end - middle, start_pos, middle, diag, outer_first, outer_last, ) aStart = start_pos + outer_first[0] bStart = middle + diag - outer_last[0] aCount = tvm.te.min(middle - aStart, step_count) bCount = tvm.te.min(end - bStart, step_count) inner_diag = tx * thread_work get_merge_begin( source, base_idx, aCount, bCount, aStart, bStart, inner_diag, first, last ) serial_merge( source, dest, source_idx, dest_idx, base_idx, aCount, bCount, aStart, bStart, start_pos + diag, inner_diag, thread_work, first, last, i_buf, j_buf, ) with T.Else(): get_merge_begin( dest, base_idx, middle - start_pos, end - middle, start_pos, middle, diag, outer_first, outer_last, ) aStart = start_pos + outer_first[0] bStart = middle + diag - outer_last[0] aCount = tvm.te.min(middle - aStart, step_count) bCount = tvm.te.min(end - bStart, step_count) inner_diag = tx * thread_work get_merge_begin( dest, base_idx, aCount, bCount, aStart, bStart, inner_diag, first, last ) serial_merge( dest, source, dest_idx, source_idx, base_idx, aCount, bCount, aStart, bStart, start_pos + diag, inner_diag, thread_work, first, last, i_buf, j_buf, ) with T.serial(0, cast(upper_lim - lower_lim, target_dtype)) as l2_width: width = 2 << (l2_width + lower_lim) # Define and launch the CUDA kernel target = tvm.target.Target.current() if "vulkan" in str(target): ntx = max_threads nbx = cast(ceil_div(width, max_threads * thread_work), "int32") nbz = cast(ceil_div(size, width), "int32") else: ntx = cast(tvm.te.min(max_threads, width), "int32") nbx = cast(ceil_div(width, max_threads * thread_work), "int32") nbz = cast(ceil_div(size, width), "int32") tx, bx, by, _, _, _ = _get_threads(ntx, nbx, nthread_by * nbz) with T.frame_scope( [ T.attr(tx, "thread_extent", ntx), T.attr(bx, "thread_extent", nbx), T.attr(by, "thread_extent", nthread_by * nbz), ] ): by_val = by % nthread_by bz = by // nthread_by base_idx = by_val * size # calculate the start, mid, and end points of this section start_pos = width * bz middle = cast(tvm.te.min(start_pos + tvm.tirx.indexdiv(width, 2), size), target_dtype) end = cast(tvm.te.min(start_pos + width, size), target_dtype) with T.If(start_pos < size): with T.Then(): even = tvm.tirx.indexmod(l2_width, 2) == 0 with T.If(nbx == 1): with T.Then(): ## merge the start->middle and middle->end arrays aCount = middle - start_pos bCount = end - middle mergepath( keys, keys_swap, values, values_swap, base_idx, aCount, bCount, start_pos, middle, start_pos, tx, ceil_div(width, ntx), even, ) with T.Else(): dual_mergepath( keys, keys_swap, values, values_swap, base_idx, start_pos, middle, end, bx, tx, max_threads * thread_work, even, ) ## if the final sorted data ended up in the swap, copy it to the real output nthread_bx = ceil_div(size, nthread_tx) with T.If( tvm.tirx.all(upper_lim > lower_lim, tvm.tirx.indexmod(upper_lim - lower_lim, 2) == 1) ): with T.Then(): tx2, bx2, by2, _, _, _ = _get_threads(nthread_tx, nthread_bx, nthread_by) with T.frame_scope( [ T.attr(tx2, "thread_extent", nthread_tx), T.attr(bx2, "thread_extent", nthread_bx), T.attr(by2, "thread_extent", nthread_by), ] ): tid = bx2 * nthread_tx + tx2 idx = by2 * size + tid with T.If(tid < size): with T.Then(): keys[idx] = keys_swap[idx] if values is not None: values[idx] = values_swap[idx] def sort_ir( data, values_out, values_out_swap, axis, is_ascend, indices_out=None, indices_out_swap=None ): """Low level IR to do sorting on the GPU, same usage as tvm.contrib.sort.argsort on the CPU. Parameters ---------- data: Buffer Buffer of input data. Data will be sorted in place. values_out : Buffer Output buffer of values of sorted tensor with same shape as data. values_out_swap : Buffer Output buffer of values with same shape as data to use as swap. axis : Int Axis long which to sort the input tensor. is_ascend : Boolean Whether to sort in ascending or descending order. indicess_out : Buffer Output buffer of indices of sorted tensor with same shape as data. indices_out_swap : Buffer Output buffer of indices with same shape as data to use as swap. Returns ------- stmt : Stmt The result IR statement. """ with IRBuilder() as ib: shape = data.shape data = T.buffer_proxy(data) values_out = T.buffer_proxy(values_out) values_out_swap = T.buffer_proxy(values_out_swap) if indices_out is not None: indices_out_orig = indices_out indices_out = T.buffer_proxy(indices_out) assert indices_out_swap is not None indices_out_swap = T.buffer_proxy(indices_out_swap) with T.If(shape[axis] > 0): with T.Then(): axis_mul_before, axis_mul_after = _sort_init( shape, axis, data, values_out, indices_out, value_init_func=( lambda _, tid: ( cast(tid, indices_out_orig.dtype) if indices_out is not None else None ) ), ) _sort_common( shape[axis], axis_mul_before, axis_mul_after, is_ascend, values_out, values_out_swap, values=indices_out, values_swap=indices_out_swap, ) return ib.get() def sort(data, axis=-1, is_ascend=1): """Performs sorting along the given axis and returns an array of sorted values with the same shape as the input data. Parameters ---------- data: tvm.te.Tensor The input array. axis : int, optional Axis long which to sort the input tensor. is_ascend : boolean, optional Whether to sort in ascending or descending order. Returns ------- out : tvm.te.Tensor The output of this function. """ ndim = len(data.shape) axis = ndim + axis if axis < 0 else axis if axis != ndim - 1: # Prepare for sorting along axis -1. axes = swap(list(range(ndim)), axis) data = transpose(data, axes) value_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "value_buf", data_alignment=8, layout=None ) value_buf_swap = tvm.tirx.decl_buffer( data.shape, data.dtype, "value_buf_swap", data_alignment=8, layout=None ) out = te.extern( [data.shape, data.shape], [data], lambda ins, outs: sort_ir(ins[0], outs[0], outs[1], -1, is_ascend), out_buffers=[value_buf, value_buf_swap], name="sort_gpu", tag="sort_gpu", )[0] if axis != ndim - 1: axes = swap(list(range(ndim)), axis) out = transpose(out, axes) return out def sort_thrust(data, axis=-1, is_ascend=1, workspace=None): """Performs sorting along the given axis and returns an array of sorted values with the same shape as the input data. Parameters ---------- data: tvm.te.Tensor The input array. axis : int, optional Axis long which to sort the input tensor. is_ascend : boolean, optional Whether to sort in ascending or descending order. workspace: Optional[tvm.te.Tensor] A buffer to store intermediate results. The size of the workspace should be sufficiently large, this can be obtained by overestimation or memory usage profiling. If None, it will fallback to use thrust internal memory allocation. Returns ------- out : tvm.te.Tensor The output of this function. """ dtype = "float32" ndim = len(data.shape) axis = ndim + axis if axis < 0 else axis if axis != ndim - 1: # Prepare for sorting along axis -1. axes = swap(list(range(ndim)), axis) data = transpose(data, axes) value_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "value_buf", data_alignment=8, layout=None ) indices_buf = tvm.tirx.decl_buffer(data.shape, dtype, "out_buf", data_alignment=8, layout=None) def f_compute(ins, outs): args = ["tvm.contrib.thrust.sort", ins[0], outs[0], outs[1], is_ascend] if workspace is not None: args.append(ins[1]) return tvm.tirx.call_packed(*args) out = te.extern( [data.shape, data.shape], [data] if workspace is None else [data, workspace], ## TODO(mbrookhart): This thrust function is actually doing argsort, not sort ## For performance, we should probably rename the contrib function and add ## a pure sort f_compute, out_buffers=[value_buf, indices_buf], name="sort_gpu", tag="sort_gpu", )[0] if axis != ndim - 1: axes = swap(list(range(ndim)), axis) out = transpose(out, axes) return out def argsort(data, axis=-1, is_ascend=1, dtype="float32", ret_type="indices"): """Performs sorting along the given axis and returns an array of indices having same shape as an input array that index data in sorted order. Parameters ---------- data: tvm.te.Tensor The input array. axis : int, optional Axis long which to sort the input tensor. is_ascend : boolean, optional Whether to sort in ascending or descending order. dtype : string, optional DType of the output indices. ret_type : string, optional The return type [both, indices]. "both": return both sorted data and indices. "indices": return sorted indices only. Returns ------- out : tvm.te.Tensor The output of this function. """ ndim = len(data.shape) axis = ndim + axis if axis < 0 else axis if axis != ndim - 1: # Prepare for sorting along axis -1. axes = swap(list(range(ndim)), axis) data = transpose(data, axes) value_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "value_buf", data_alignment=8, layout=None ) value_swap_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "value_swap_buf", data_alignment=8, layout=None ) indices_buf = tvm.tirx.decl_buffer(data.shape, dtype, "out_buf", data_alignment=8, layout=None) indices_swap_buf = tvm.tirx.decl_buffer( data.shape, dtype, "out_swap_buf", data_alignment=8, layout=None ) outs = te.extern( [data.shape, data.shape, data.shape, data.shape], [data], lambda ins, outs: sort_ir( ins[0], outs[0], outs[2], -1, is_ascend, indices_out=outs[1], indices_out_swap=outs[3], ), out_buffers=[value_buf, indices_buf, value_swap_buf, indices_swap_buf], name="argsort_gpu", tag="argsort_gpu", ) if axis != ndim - 1: axes = swap(list(range(ndim)), axis) outs = [transpose(out, axes) for out in outs] if ret_type == "indices": return outs[1] return outs[0], outs[1] def argsort_thrust(data, axis=-1, is_ascend=1, dtype="float32", ret_type="indices", workspace=None): """Performs sorting along the given axis and returns an array of indices having same shape as an input array that index data in sorted order. Parameters ---------- data: tvm.te.Tensor The input array. axis : int, optional Axis long which to sort the input tensor. is_ascend : boolean, optional Whether to sort in ascending or descending order. dtype : string, optional DType of the output indices. ret_type : string, optional The return type [both, indices]. "both": return both sorted data and indices. "indices": return sorted indices only. workspace : Optional[tvm.te.Tensor] A buffer to store intermediate results. The size of the workspace should be sufficiently large, this can be obtained by overestimation or memory usage profiling. If None, it will fallback to use thrust internal memory allocation. Returns ------- out : tvm.te.Tensor The output of this function. """ return topk_thrust(data, 0, axis, ret_type, is_ascend, dtype, workspace) def topk(data, k=1, axis=-1, ret_type="both", is_ascend=False, dtype="int64"): """Get the top k elements in an input tensor along the given axis. Parameters ---------- data : tvm.te.Tensor The input tensor. k : int, optional Number of top elements to select. Return all elements if k < 1. axis : int, optional Axis long which to sort the input tensor. ret_type: str, optional The return type [both, values, indices]. "both": return both top k data and indices. "values": return top k data only. "indices": return top k indices only. is_ascend : boolean, optional Whether to sort in ascending or descending order. dtype : string, optional The data type of the indices output. Returns ------- out : tvm.te.Tensor or List[tvm.te.Tensor] The computed result. """ assert ret_type in ["both", "values", "indices"] ndim = len(data.shape) axis = axis + ndim if axis < 0 else axis assert 0 <= axis < ndim dshape = data.shape if axis != ndim - 1: axes = swap(list(range(ndim)), axis) data = transpose(data, axes) values_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "values_buf", data_alignment=8, layout=None ) values_swap_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "values_swap_buf", data_alignment=8, layout=None ) indices_buf = tvm.tirx.decl_buffer( data.shape, dtype, "indices_buf", data_alignment=8, layout=None ) indices_swap_buf = tvm.tirx.decl_buffer( data.shape, dtype, "indies_swap_buf", data_alignment=8, layout=None ) if ret_type == "values": output = te.extern( [data.shape, data.shape], [data], lambda ins, outs: sort_ir(ins[0], outs[0], outs[1], -1, is_ascend), out_buffers=[values_buf, values_swap_buf], name="topk_gpu", tag="topk_gpu", )[0] if axis != ndim - 1: axes = swap(list(range(ndim)), axis) output = transpose(output, axes) else: output = te.extern( [data.shape, data.shape, data.shape, data.shape], [data], lambda ins, outs: sort_ir(ins[0], outs[0], outs[2], -1, is_ascend, outs[1], outs[3]), out_buffers=[values_buf, indices_buf, values_swap_buf, indices_swap_buf], name="topk_gpu", tag="topk_gpu", )[0:2] if axis != ndim - 1: axes = swap(list(range(ndim)), axis) output[0] = transpose(output[0], axes) output[1] = transpose(output[1], axes) if isinstance(k, int) and k < 1: if ret_type == "indices": return output[1] return output beg = [0] * ndim end = [] strides = [1] * ndim for i in range(ndim): if i == axis: end.append(k if isinstance(k, int) else tvm.te.var("dim")) else: end.append(dshape[i]) if ret_type == "both": values_out, indices_out = output values_out = strided_slice(values_out, beg, end, strides) indices_out = strided_slice(indices_out, beg, end, strides) output = [values_out, indices_out] elif ret_type == "values": output = [strided_slice(output, beg, end, strides)] else: # ret_type == "indices" indices_out = output[1] output = [strided_slice(indices_out, beg, end, strides)] return output def topk_thrust( data, k=1, axis=-1, ret_type="both", is_ascend=False, dtype="int64", workspace=None ): """Get the top k elements in an input tensor along the given axis. Parameters ---------- data : tvm.te.Tensor The input tensor. k : int, optional Number of top elements to select. Return all elements if k < 1. axis : int, optional Axis long which to sort the input tensor. ret_type: str, optional The return type [both, values, indices]. "both": return both top k data and indices. "values": return top k data only. "indices": return top k indices only. is_ascend : boolean, optional Whether to sort in ascending or descending order. dtype : string, optional The data type of the indices output. workspace : Optional[tvm.te.Tensor] A buffer to store intermediate results. The size of the workspace should be sufficiently large, this can be obtained by overestimation or memory usage profiling. If None, it will fallback to use thrust internal memory allocation. Returns ------- out : tvm.te.Tensor or List[tvm.te.Tensor] The computed result. """ assert ret_type in ["both", "values", "indices"] ndim = len(data.shape) axis = ndim + axis if axis < 0 else axis if axis != ndim - 1: # Prepare for sorting along axis -1. axes = swap(list(range(ndim)), axis) data = transpose(data, axes) data_buf = tvm.tirx.decl_buffer( data.shape, data.dtype, "data_buf", data_alignment=8, layout=None ) if workspace is not None: workspace_buf = tvm.tirx.decl_buffer( workspace.shape, workspace.dtype, "workspace_buf", data_alignment=8, layout=None ) else: workspace_buf = None out_bufs = [ tvm.tirx.decl_buffer(data.shape, data.dtype, "value_buf", data_alignment=8, layout=None), tvm.tirx.decl_buffer(data.shape, dtype, "indices_buf", data_alignment=8, layout=None), ] def f_compute(ins, outs): args = ["tvm.contrib.thrust.sort", ins[0], outs[0], outs[1], is_ascend] if workspace is not None: args.append(ins[1]) return tvm.tirx.call_packed(*args) is_ascend = 1 if is_ascend else 0 out = te.extern( [data.shape, data.shape], [data] if workspace is None else [data, workspace], f_compute, in_buffers=[data_buf] if workspace is None else [data_buf, workspace_buf], out_buffers=out_bufs, name="topk_gpu", tag="topk_gpu", ) if isinstance(k, tvm.tirx.IntImm): k = k.value if not isinstance(k, int) or k > 0: beg = [0] * ndim end = data.shape[:-1] + [k if isinstance(k, int) else tvm.te.var("dim")] strides = [1] * ndim out = [strided_slice(o, beg, end, strides) for o in out] if axis != ndim - 1: axes = swap(list(range(ndim)), axis) out = [transpose(o, axes) for o in out] if ret_type == "values": out = out[0] elif ret_type == "indices": out = out[1] return out def searchsorted(sorted_sequence, values, right=False, out_dtype="int64"): """Find indices where elements should be inserted to maintain order. If `sorted_sequence` is N-dimensional, the innermost dimension of `values` are searched in the corresponding dimension of `sorted_sequence`. This implementation is optimized for GPU execution. Parameters ---------- sorted_sequence : te.Tensor N-D or 1-D Tensor, containing monotonically increasing sequence on the innermost dimension. values : te.Tensor N-D Tensor containing the search values. When `sorted_sequence` is 1-D, the shape of `values` can be arbitrary. Otherwise, ranks of `sorted_sequence` and `values` must be the same, and outer N-1 axes must have the same size. right : bool, optional Controls which index is returned if a value lands exactly on one of sorted values. If False (side='left'), the index of the first suitable location found is given. If true (side='right'), return the last such index. out_dtype : string, optional The data type of the output indices. Returns ------- indices : te.Tensor Tensor with same shape as values, representing the indices of elements of `values` if they are inserted in `sorted_sequence`. """ if len(sorted_sequence.shape) > 1: for i in range(len(values.shape) - 1): assert values.shape[i] == sorted_sequence.shape[i], ( "Outer dimensions of sorted_sequence and values must match for N-D searchsorted" ) def ir(sorted_sequence_buf, values_buf, indices_buf): with IRBuilder() as ib: sorted_sequence_shape = sorted_sequence_buf.shape values_shape = values_buf.shape num_search = prod(values_shape) search_range = sorted_sequence_shape[-1] sorted_sequence_ptr = T.buffer_proxy(sorted_sequence_buf) values_ptr = T.buffer_proxy(values_buf) indices_ptr = T.buffer_proxy(indices_buf) max_threads = int(tvm.target.Target.current(allow_none=False).attrs["max_num_threads"]) nthread_tx = max_threads nthread_bx = ceil_div(num_search, nthread_tx) tx = te.thread_axis("threadIdx.x") bx = te.thread_axis("blockIdx.x") with T.frame_scope( [ T.attr(tx, "thread_extent", nthread_tx), T.attr(bx, "thread_extent", nthread_bx), ] ): tid = bx * nthread_tx + tx with T.If(tid < num_search): with T.Then(): if len(sorted_sequence_shape) == 1: sequence_offset = 0 else: sequence_id = tid // values_shape[-1] sequence_offset = sequence_id * search_range indices_ptr[tid] = binary_search( sequence_offset, search_range, sorted_sequence_ptr, values_ptr[tid], right, out_dtype, ) return ib.get() return te.extern( values.shape, [sorted_sequence, values], lambda ins, outs: ir(ins[0], ins[1], outs[0]), name="searchsorted_gpu", dtype=out_dtype, )