Files
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

370 lines
12 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
# ruff: noqa: RUF005
"""Default legalization function for manipulate operators."""
import tvm
from tvm import DataTypeCode, relax, s_tir, te, tirx, topi
from tvm.ir import Call
from tvm.relax.op.base import call_tir
from tvm.relax.type import TensorType
from tvm.relax.utils import gen_call_tir_inputs
from tvm.tirx.expr import IntImm
from ...block_builder import BlockBuilder
from ...expr import Expr, ShapeExpr, Tuple, TupleGetItem, Var
from .common import LegalizeFunc, TEFunc, register_legalize
def _reshape(
te_func: TEFunc, primfunc_name: str, is_collapse_sum_like: bool = False
) -> LegalizeFunc:
def reshape_call_te(bb: BlockBuilder, call: Call):
tgt_shape = call.args[1].ty.shape if is_collapse_sum_like else call.args[1]
# If target shape is Var, pass its bound expr only when it is ShapeExpr
if isinstance(tgt_shape, Var):
tgt_shape = bb.lookup_binding(tgt_shape)
assert isinstance(tgt_shape, ShapeExpr)
return bb.call_te(te_func, call.args[0], tgt_shape, primfunc_name_hint=primfunc_name)
return reshape_call_te
register_legalize("relax.broadcast_to", _reshape(topi.broadcast_to, "broadcast_to"))
register_legalize("relax.reshape", _reshape(topi.reshape, "reshape"))
register_legalize(
"relax.collapse_sum_like",
_reshape(topi.collapse_sum, "collapse_sum", is_collapse_sum_like=True),
)
register_legalize("relax.collapse_sum_to", _reshape(topi.collapse_sum, "collapse_sum"))
@register_legalize("relax.concat")
def _concat(bb: BlockBuilder, call: Call) -> Expr:
t = call.args[0]
n_field = len(t.ty.fields)
while isinstance(t, Var):
binding = bb.lookup_binding(t)
if not isinstance(binding, Tuple | Var):
break
t = binding
assert isinstance(t, Tuple | Var)
fields = (
t.fields if isinstance(t, Tuple) else [bb.emit(TupleGetItem(t, i)) for i in range(n_field)]
)
return bb.call_te(
topi.concatenate, fields, None if call.attrs.axis is None else call.attrs.axis
)
@register_legalize("relax.expand_dims")
def _expand_dims(bb: BlockBuilder, call: Call) -> Expr:
def te_expand_dims(data, axis):
data_relax = relax.Var("data", relax.TensorType(data.shape))
f_infer_ty = call.op.get_attr("FInferType")
output_shape = f_infer_ty(relax.op.expand_dims(data_relax, axis), bb).shape
output_ndim = len(output_shape)
data_dims = []
for i in range(output_ndim):
if i not in axis and (i - output_ndim) not in axis:
data_dims.append(i)
return te.compute(
output_shape,
lambda *idx: data(*[idx[dim] for dim in data_dims]),
name="expand_dims",
)
return bb.call_te(
te_expand_dims, call.args[0], call.attrs.axis, primfunc_name_hint="expand_dims"
)
@register_legalize("relax.flatten")
def _flatten(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.reshape, call.args[0], call.ty.shape.values)
@register_legalize("relax.permute_dims")
def _permute_dims(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.transpose, call.args[0], call.attrs.axes)
@register_legalize("relax.split")
def _split(bb: BlockBuilder, call: Call) -> Expr:
if isinstance(call.attrs.indices_or_sections, tirx.IntImm):
indices_or_sections = call.attrs.indices_or_sections.value
else:
indices_or_sections = call.attrs.indices_or_sections
return bb.call_te(topi.split, call.args[0], indices_or_sections, call.attrs.axis)
@register_legalize("relax.squeeze")
def _squeeze(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.squeeze, call.args[0], call.attrs.axis)
@register_legalize("relax.stack")
def _stack(bb: BlockBuilder, call: Call) -> Expr:
t = call.args[0]
n_field = len(t.ty.fields)
# Follow bindings to find the actual tuple
while isinstance(t, Var):
binding = bb.lookup_binding(t)
if not isinstance(binding, Tuple | Var):
break
t = binding
assert isinstance(t, Tuple | Var)
# Extract fields from either Tuple or bound Var
fields = (
t.fields if isinstance(t, Tuple) else [bb.emit(TupleGetItem(t, i)) for i in range(n_field)]
)
return bb.call_te(topi.stack, fields, 0 if call.attrs.axis is None else call.attrs.axis)
@register_legalize("relax.repeat")
def _repeat(bb: BlockBuilder, call: Call) -> Expr:
def te_repeat(data: te.Tensor, repeats: IntImm, axis: IntImm | None):
if axis is None:
# flatten data
out_shape = data.shape[0]
for i in data.shape[1:]:
out_shape *= i
data = topi.reshape(data, (out_shape,))
axis = 0
# topi only receives int repeats and axis
return topi.repeat(data, int(repeats), int(axis))
return bb.call_te(
te_repeat, call.args[0], call.attrs.repeats, call.attrs.axis, primfunc_name_hint="repeat"
)
@register_legalize("relax.tile")
def _tile(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.tile, call.args[0], call.attrs.repeats)
@register_legalize("relax.flip")
def _flip(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.flip, call.args[0], int(call.attrs.axis))
@register_legalize("relax.reverse_sequence")
def _reverse_sequence(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.reverse_sequence,
call.args[0],
call.args[1],
int(call.attrs.seq_axis),
int(call.attrs.batch_axis),
primfunc_name_hint="reverse_sequence",
)
@register_legalize("relax.gather_elements")
def _gather_elements(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.gather, call.args[0], int(call.attrs.axis), call.args[1])
@register_legalize("relax.gather_nd")
def _gather_nd(bb: BlockBuilder, call: Call) -> Expr:
def te_gather_nd(data, indices, batch_dims):
indices_ndim = len(indices.shape)
axes = [indices_ndim - 1] + list(range(indices_ndim - 1))
indices = topi.transpose(indices, axes)
return topi.gather_nd(data, indices, batch_dims)
return bb.call_te(te_gather_nd, call.args[0], call.args[1], int(call.attrs.batch_dims))
@register_legalize("relax.index_tensor")
def _index_tensor(bb: BlockBuilder, call: Call) -> Expr:
t = call.args[1]
n_field = len(t.ty.fields)
fields = [bb.emit(TupleGetItem(t, i)) for i in range(n_field)]
return bb.call_te(topi.index_tensor, call.args[0], fields)
@register_legalize("relax.index_put")
def _index_put(bb: BlockBuilder, call: Call) -> Expr:
data = call.args[0]
indices = call.args[1]
values = call.args[2]
accumulate = call.attrs.accumulate
# If indices is a Tuple, unpack it into individual tensors
if isinstance(indices, relax.Tuple):
indices_list = [indices.fields[i] for i in range(len(indices.fields))]
else:
indices_list = [indices]
return bb.call_te(
topi.index_put,
data,
indices_list,
values,
accumulate=accumulate,
)
@register_legalize("relax.meshgrid")
def _meshgrid(bb: BlockBuilder, call: Call) -> Expr:
t = call.args[0]
n_field = len(t.ty.fields)
while isinstance(t, Var):
binding = bb.lookup_binding(t)
if not isinstance(binding, Tuple | Var):
break
t = binding
assert isinstance(t, Tuple | Var)
fields = (
t.fields if isinstance(t, Tuple) else [bb.emit(TupleGetItem(t, i)) for i in range(n_field)]
)
return bb.call_te(
topi.meshgrid, fields, "ij" if call.attrs.indexing is None else call.attrs.indexing
)
def _is_gpu_target():
target = tvm.target.Target.current(allow_none=True)
return target is not None and "gpu" in target.keys
@register_legalize("relax.scatter_elements")
def _scatter_elements(bb: BlockBuilder, call: Call) -> Expr:
te_func = topi.gpu.scatter_elements if _is_gpu_target() else topi.scatter_elements
return bb.call_te(
te_func,
call.args[0],
call.args[1],
call.args[2],
call.attrs.axis,
call.attrs.reduction,
)
@register_legalize("relax.scatter_nd")
def _scatter_nd(bb: BlockBuilder, call: Call) -> Expr:
# TODO(relax-team): Support native scatter_nd without te extern
base_te = topi.gpu.scatter_nd if _is_gpu_target() else topi.scatter_nd
def scatter_nd(data, indices, updates, reduction):
axes = list(range(len(indices.shape)))
indices = topi.transpose(indices, axes[-1:] + axes[:-1])
return base_te(data, indices, updates, reduction)
return bb.call_te(
scatter_nd,
call.args[0],
call.args[1],
call.args[2],
call.attrs.reduction,
)
@register_legalize("relax.slice_scatter")
def _slice_scatter(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.slice_scatter,
call.args[0],
call.args[1],
call.args[2],
call.args[3],
call.args[4],
call.attrs.axis,
)
@register_legalize("relax.one_hot")
def _one_hot(bb: BlockBuilder, call: Call) -> Expr:
indices, on_value, off_value = call.args
if not (tvm.ir.is_prim_expr(on_value) and tvm.ir.is_prim_expr(off_value)):
raise ValueError("on_value and off_value must be Expr")
if on_value.ty != off_value.ty:
raise ValueError("on_value and off_value must have the same dtype")
return bb.call_te(
topi.one_hot,
indices,
on_value,
off_value,
call.attrs.depth,
call.attrs.axis,
on_value.ty,
)
@register_legalize("relax.layout_transform")
def _layout_transform(bb: BlockBuilder, call: Call) -> Expr:
def te_layout_transform(data, name):
"""
Returns a passthrough TE compute with appropriate name. This is needed to generate
TIR function, output shape info, TIR vars from gen_call_tir_inputs function.
"""
return te.compute(
data.shape,
data,
name=name,
)
def set_axis_sep(axis_sep: list, sch: s_tir.schedule, buffer_type: str):
sch.set_axis_separator(primfunc_name, (buffer_type, 0), axis_separators=axis_sep)
index_map: tvm.tirx.IndexMap = call.attrs.index_map
pad_value = call.attrs.pad_value
if pad_value is not None:
pad_value = pad_value.value
else:
if call.args[0].ty.dtype.matches_code(DataTypeCode.INT, DataTypeCode.UINT):
pad_value = 0
else:
pad_value = 0.0
axis_separators: tvm.tirx.IndexMap.AXIS_SEPARATOR = call.attrs.axis_separators
input_axis_separators: tvm.tirx.IndexMap.AXIS_SEPARATOR = call.attrs.input_axis_separators
# Convert to list from array
axis_separators = [int(sep) for sep in axis_separators]
primfunc_name = "te_layout_transform"
_, padding_predicate = index_map.non_surjective_inverse(call.args[0].ty.shape)
if not isinstance(padding_predicate, tvm.tirx.expr.IntImm):
primfunc_name += "_with_pad"
if len(axis_separators) != 0:
primfunc_name += "_axis_separator"
tir_func, call_args, _, tir_vars = gen_call_tir_inputs(
te_layout_transform, call.args[0], primfunc_name
)
# Create TIR schedule to apply layout changes with axis separators
sch = tvm.s_tir.Schedule(tir_func)
sch.transform_layout(primfunc_name, ("write", 0), index_map, pad_value)
set_axis_sep(axis_separators, sch, "write")
if input_axis_separators is not None:
set_axis_sep(input_axis_separators, sch, "read")
gvar = bb.add_func(sch.mod["main"], primfunc_name)
output_shape = index_map.map_shape(list(call_args[0].ty.shape))
output_dtype = call_args[0].ty.dtype
output_ty = [TensorType(output_shape, output_dtype)]
return call_tir(gvar, call_args, output_ty, tir_vars)