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

180 lines
5.8 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
"""Default legalization function for statistical operators."""
from collections.abc import Callable
from tvm import te, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr, ShapeExpr
from .common import LegalizeFunc, TEFunc, register_legalize
def _normalize_reduction_axes(axis: list[int] | None, ndim: int) -> list[int]:
if axis is None:
return list(range(ndim))
axes = []
for dim in axis:
if isinstance(dim, tirx.IntImm):
dim = dim.value
dim = int(dim)
axes.append(dim + ndim if dim < 0 else dim)
return axes
def _has_const_zero_reduction_dim(call: Call) -> bool:
input_shape = call.args[0].ty.shape
if not isinstance(input_shape, ShapeExpr):
return False
axes = _normalize_reduction_axes(call.attrs.axis, len(input_shape.values))
return any(
isinstance(input_shape.values[dim], tirx.IntImm) and input_shape.values[dim] == 0
for dim in axes
)
def _statistical(
te_func: TEFunc,
zero_dim_identity: int | float | bool | Callable[[str], int | float | bool] | None = None,
) -> LegalizeFunc:
def statistical_call_te(bb: BlockBuilder, call: Call) -> Expr:
if zero_dim_identity is not None and _has_const_zero_reduction_dim(call):
fill_value = (
zero_dim_identity(call.ty.dtype)
if callable(zero_dim_identity)
else zero_dim_identity
)
return bb.call_te(
topi.full,
call.ty.shape.values,
call.ty.dtype,
fill_value,
)
return bb.call_te(te_func, call.args[0], call.attrs.axis, call.attrs.keepdims)
return statistical_call_te
def _compute_shape_prod(x: te.Tensor, axis: list[int]) -> tirx.Expr:
shape_prod = tirx.const(1, "int32")
axes = list(axis) if axis is not None else range(0, len(x.shape))
for dim in axes:
shape_prod = shape_prod * x.shape[dim]
return shape_prod
def _te_mean(x: te.Tensor, axis: list[int], keepdims: bool) -> te.Tensor:
shape_prod = _compute_shape_prod(x, axis)
res_sum = topi.sum(x, axis, keepdims)
return topi.divide(res_sum, shape_prod)
def _te_variance(x: te.Tensor, axis: list[int], keepdims: bool) -> te.Tensor:
dev = x - _te_mean(x, axis, True)
return _te_mean(dev * dev, axis, keepdims)
# This version has better memory locality and performance
# But may trigger some precision problems, so we will use the previous version now
# mean = _te_mean(x, axis, keepdims)
# return _te_mean(x * x, axis, keepdims) - mean * mean
def _te_median(
x: te.Tensor, axis: list[int], keepdims: bool
) -> te.Tensor | tuple[te.Tensor, te.Tensor]:
# currently only supports one axis or no axis ~ same pytorch
# todo: support multiple axis ~ same numpy
shape_prod = _compute_shape_prod(x, axis)
mid_index = (shape_prod - 1) // 2
if axis is None or len(axis) == 0:
x = topi.reshape(x, [shape_prod])
ax = -1
else:
ax = axis[0]
index_sorted = topi.argsort(x, axis=ax, is_ascend=True, dtype="int64")
x_sorted = topi.gather(x, axis=ax, indices=index_sorted)
new_shape = list(x.shape)
new_shape[ax] = 1
indices = topi.full(new_shape, fill_value=mid_index, dtype="int64")
median_val = topi.gather(x_sorted, axis=ax, indices=indices)
median_idx = topi.gather(index_sorted, axis=ax, indices=indices)
if axis is None or len(axis) == 0:
return median_val if keepdims else topi.squeeze(median_val, axis=axis)
val = median_val
idx = median_idx
if not keepdims:
val = topi.squeeze(val, axis=axis)
idx = topi.squeeze(idx, axis=axis)
return val, idx
@register_legalize("relax.mean")
def _mean(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
_te_mean, call.args[0], call.attrs.axis, call.attrs.keepdims, primfunc_name_hint="mean"
)
@register_legalize("relax.std")
def _std(bb: BlockBuilder, call: Call) -> Expr:
def te_std(x: te.Tensor, axis: list[int], keepdims: bool) -> te.Tensor:
return topi.sqrt(_te_variance(x, axis, keepdims))
return bb.call_te(
te_std, call.args[0], call.attrs.axis, call.attrs.keepdims, primfunc_name_hint="std"
)
@register_legalize("relax.variance")
def _variance(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
_te_variance,
call.args[0],
call.attrs.axis,
call.attrs.keepdims,
primfunc_name_hint="variance",
)
@register_legalize("relax.median")
def _median(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
_te_median,
call.args[0],
call.attrs.axis,
call.attrs.keepdims,
primfunc_name_hint="median",
)
register_legalize("relax.max", _statistical(topi.max))
register_legalize("relax.min", _statistical(topi.min))
register_legalize(
"relax.prod",
_statistical(topi.prod, zero_dim_identity=lambda dtype: True if dtype == "bool" else 1),
)
register_legalize("relax.sum", _statistical(topi.sum, zero_dim_identity=0))