1172 lines
40 KiB
Python
1172 lines
40 KiB
Python
# 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,
|
|
)
|