chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
# isort: skip_file
# 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.
"""Relax transformations."""
from .transform import (
AdjustMatmulOrder,
AllocateWorkspace,
AlterOpImpl,
AnnotateTIROpPattern,
AttachAttrLayoutFreeBuffers,
AttachGlobalSymbol,
BindParams,
BindSymbolicVars,
BundleModelParams,
CallTIRRewrite,
CanonicalizeBindings,
CombineParallelMatmul,
ComputePrimValue,
ConvertLayout,
ConvertToDataflow,
DataflowBlockPass,
DataflowUseInplaceCalls,
DeadCodeElimination,
DecomposeOpsForInference,
DecomposeOpsForTraining,
EliminateCommonSubexpr,
ExpandMatmulOfSum,
ExpandTupleArguments,
FoldConstant,
FunctionPass,
FuseOps,
FuseOpsByPattern,
FuseTIR,
FusionPattern,
Gradient,
InlinePrivateFunctions,
KillAfterLastUse,
LambdaLift,
LazyGetInput,
LazySetOutput,
LegalizeOps,
LiftTransformParams,
LowerAllocTensor,
LowerRuntimeBuiltin,
MergeCompositeFunctions,
MetaScheduleApplyDatabase,
MetaScheduleTuneIRMod,
MetaScheduleTuneTIR,
Normalize,
NormalizeGlobalVar,
PatternCheckContext,
RealizeVDevice,
RemovePurityChecking,
RemoveUnusedOutputs,
RemoveUnusedParameters,
ReorderPermuteDimsAfterConcat,
ReorderTakeAfterMatmul,
RewriteCUDAGraph,
RewriteDataflowReshape,
RunCodegen,
SplitCallTIRByPattern,
SplitLayoutRewritePreproc,
StaticPlanBlockMemory,
ToMixedPrecision,
ToNonDataflow,
TopologicalSort,
UpdateParamType,
UpdateVDevice,
VMBuiltinLower,
VMShapeLower,
SpecializePrimFuncBasedOnCallSite,
dataflowblock_pass,
function_pass,
)
from .attach_external_modules import AttachExternModules
from .fast_math import FastMathTransform
from .fuse_transpose_matmul import FuseTransposeMatmul
from .ipc_allreduce_rewrite import IPCAllReduceRewrite
from .lazy_transform_params import LazyTransformParams
from .lower_gpu_ipc_alloc_storage import LowerGPUIPCAllocStorage
from .optimize_layout_transform import OptimizeLayoutTransform
from .fold_batch_norm_to_conv2d_for_inference import FoldBatchnormToConv2D
from .remove_redundant_reshape import RemoveRedundantReshape
# Import to register the legalization functions.
from . import legalize_ops
+20
View File
@@ -0,0 +1,20 @@
# 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
"""FFI APIs for tvm.transform"""
import tvm_ffi
tvm_ffi.init_ffi_api("relax.transform", __name__)
@@ -0,0 +1,53 @@
# 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.
"""A pass that attaches external modules to the IRModule.
Note: "external modules" here refers to `relax.frontend.nn.ExternModule`.
"""
from typing import TYPE_CHECKING
from tvm.ir import IRModule
from tvm.ir.transform import PassContext, module_pass
if TYPE_CHECKING:
from tvm.relax.frontend.nn import ExternModule
@module_pass(opt_level=0, name="AttachExternalModules")
class AttachExternModules: # pylint: disable=too-few-public-methods
"""Attach variable bounds to each Relax function, which primarily helps with memory planning."""
def __init__(self, extern_modules: list["ExternModule"]):
self.extern_modules = extern_modules
def transform_module(self, mod: IRModule, _ctx: PassContext) -> IRModule:
"""Entrypoint"""
from tvm.relax.frontend.nn import ( # pylint: disable=import-outside-toplevel
ExternModule,
)
def _load(ext_mod: ExternModule):
assert isinstance(ext_mod, ExternModule), f"Expected ExternModule, but got: {ext_mod}"
return ext_mod.load()
mod_attrs = dict(mod.attrs) if mod.attrs else {}
external_mods = mod_attrs.get("external_mods", [])
for ext in self.extern_modules:
external_mods.append(_load(ext))
mod = mod.with_attr("external_mods", external_mods)
return mod
+65
View File
@@ -0,0 +1,65 @@
# 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, unused-argument, redefined-argument-from-local
"""Relax Use Fast Math pass."""
import tvm
from tvm import topi
from tvm.ir.module import IRModule
from tvm.relax import Call, Expr, PyExprMutator, expr_functor
@expr_functor.mutator
class FastMathCodeGenerator(PyExprMutator):
"""
Converts the expensive non linear functions to their fast but approximate counterparts.
Parameters
----------
mod: IRModule
The module to be transformed
"""
def __init__(self, mod):
super().__init__(mod)
def visit_call_(self, call: Call) -> Expr:
if call.op.name == "relax.nn.softmax":
return self.builder_.call_te(topi.nn.fast_softmax, call.args[0], call.attrs.axis)
if call.op.name == "relax.exp":
return self.builder_.call_te(topi.fast_exp, call.args[0])
if call.op.name == "relax.erf":
return self.builder_.call_te(topi.fast_erf, call.args[0])
if call.op.name == "relax.tanh":
return self.builder_.call_te(topi.fast_tanh, call.args[0])
return super().visit_call_(call)
@tvm.transform.module_pass(opt_level=0, name="FastMathTransform")
class FastMathTransform:
"""
Pass to convert the expensive non linear functions to their fast but approximate counterparts.
"""
def transform_module(self, mod: IRModule, ctx: tvm.transform.PassContext) -> IRModule:
fast_math_codegen = FastMathCodeGenerator(mod)
for gv, func in mod.functions_items():
if isinstance(func, tvm.relax.Function):
func = fast_math_codegen.visit_expr(func)
fast_math_codegen.builder_.update_func(gv, func)
return fast_math_codegen.builder_.get()
@@ -0,0 +1,103 @@
# 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, unused-argument, redefined-argument-from-local
"""Relax Fold Batchnorm into Conv2D."""
from tvm import relax, tirx
from tvm.ir.module import IRModule
from tvm.ir.transform import PassContext
from tvm.relax import Expr
from tvm.relax.dpl import TupleGetItemPattern, is_const, is_op, rewrite_call, wildcard
from . import function_pass
@function_pass(opt_level=0)
class FoldBatchnormToConv2D:
"""
Fuse Batchnorm to its previous Conv2D
This optimization is a special case of FoldScaleAxis that folds scale into conv2d weights.
This pass can be removed when FoldScaleAcis enhances to support this case.
"""
def __init__(self):
self.input = wildcard()
self.weight = is_const()
self.pattern_conv2d = is_op("relax.nn.conv2d")(self.input, self.weight)
self.bn_weight = is_const()
self.bias = is_const()
self.mean = is_const()
self.variance = is_const()
self.pattern_bn = is_op("relax.nn.batch_norm")(
self.pattern_conv2d, self.bn_weight, self.bias, self.mean, self.variance
)
self.pattern = TupleGetItemPattern(self.pattern_bn, 0)
def transform_function(self, func: Expr, mod: IRModule, ctx: PassContext) -> IRModule:
"""
Tranformation function for pattern Conv2D+BatchNorm+TupleGetItem pattern
Parameters
----------
func: Expr
The relax function to be optimized
mod: IRModule
The ir module
ctx: PassContext
Relax pass context
"""
self.mod = mod
updated_call = func
# Skip primitive functions
if "Primitive" in func.attrs.keys() and func.attrs["Primitive"] != 0:
return updated_call
def rewriter(expr, matches):
conv_input = matches[self.input]
conv_weight = matches[self.weight]
bn_weight = matches[self.bn_weight]
bn_bias = matches[self.bias]
bn_mean = matches[self.mean]
bn_variance = matches[self.variance]
conv_op = matches[self.pattern_conv2d]
bn_op = matches[self.pattern_bn]
conv_attrs = conv_op.attrs
bn_attrs = bn_op.attrs
bn_variance = relax.op.add(
bn_variance, relax.prim_value(tirx.FloatImm("float32", bn_attrs["epsilon"]))
)
dino = relax.op.sqrt(bn_variance)
wt = relax.op.divide(bn_weight, dino)
bs = relax.op.subtract(bn_bias, relax.op.multiply(bn_mean, wt))
if conv_attrs["kernel_layout"] == "OIHW":
wt = relax.op.reshape(wt, shape=(bn_weight.ty.shape[0], 1, 1, 1))
elif conv_attrs["kernel_layout"] == "IOHW":
wt = wt.reshape(1, bn_weight.ty.shape[0], 1, 1)
else:
return expr
wt_conv = relax.op.multiply(conv_weight, wt)
bs_args = relax.op.reshape(bs, shape=(1, bn_bias.ty.shape[0], 1, 1))
conv_out = relax.Call(conv_op.op, (conv_input, wt_conv), conv_attrs)
return relax.op.add(conv_out, bs_args)
updated_call = rewrite_call(self.pattern, rewriter, func)
return updated_call
@@ -0,0 +1,174 @@
# 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.
"""A compiler pass that fuses transpose + matmul and generate TIR function.
Note that
1. Please put the pass before LegalizeOps pass.
2. The pass only works for XW^T but not X^TW
3. The pass would rewrite the relax ops into TIR functions. If you'd like to dispatch the
ops into library (e.g. cuBLAS) calls, please run dispatch pass before this pass.
"""
import tvm
from tvm import IRModule, relax, te, tirx
from tvm.relax.dpl.pattern import is_op, wildcard
from tvm.relax.expr_functor import PyExprMutator, mutator
@tvm.transform.module_pass(opt_level=0, name="FuseTransposeMatmul")
class FuseTransposeMatmul: # pylint: disable=too-few-public-methods
"""A compiler pass that fuses transpose + matmul."""
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
mod = relax.transform.FuseOpsByPattern(
[
(
"transpose_matmul_fuse",
*_pattern(),
),
],
bind_constants=False,
)(mod)
transpose_matmul_codegen = _TransposeMatmulFuser(mod)
for g_var, func in mod.functions_items():
if isinstance(func, relax.Function):
func = transpose_matmul_codegen.visit_expr(func)
transpose_matmul_codegen.builder_.update_func(g_var, func)
return transpose_matmul_codegen.builder_.get()
def _pattern():
"""Pattern for transpose + matmul."""
# pylint: disable=invalid-name
w = wildcard()
x = wildcard()
wT = is_op("relax.permute_dims")(w)
o = is_op("relax.matmul")(x, wT)
# pylint: enable=invalid-name
annotations = {"o": o, "w": w, "x": x, "wT": wT}
def _check(context: relax.transform.PatternCheckContext) -> bool:
transpose_call = context.annotated_expr["wT"]
ndim = transpose_call.args[0].ty.ndim
if ndim == -1:
return False
if ndim == 2 and transpose_call.attrs.axes is None:
return True
axes = list(range(ndim))
axes[-1], axes[-2] = axes[-2], axes[-1]
return list(transpose_call.attrs.axes) == axes
return o, annotations, _check
# pylint: disable=missing-docstring,invalid-name
@mutator
class _TransposeMatmulFuser(PyExprMutator): # pylint: disable=abstract-method
def __init__(self, mod):
super().__init__(mod)
def visit_call_( # pylint: disable=arguments-renamed
self,
call: relax.Call,
) -> relax.Expr:
out_dtype = None
def te_transposed_matmul(a: te.Tensor, b: te.Tensor) -> te.Tensor:
nonlocal out_dtype
a_shape = list(a.shape)
b_shape = list(b.shape)
a_prepended = False
b_appended = False
if len(a_shape) == 1:
a_prepended = True
a_shape.insert(0, 1)
if len(b_shape) == 1:
b_appended = True
b_shape.append(1)
is_a_larger = len(a_shape) > len(b_shape)
offset = len(a_shape) - len(b_shape) if is_a_larger else len(b_shape) - len(a_shape)
a_relax = relax.Var("a", relax.TensorType(a.shape))
bT_shape = list(b.shape)
bT_shape[-1], bT_shape[-2] = bT_shape[-2], bT_shape[-1]
bT_relax = relax.Var("b", relax.TensorType(bT_shape))
output_shape = self.builder_.normalize(relax.op.matmul(a_relax, bT_relax)).ty.shape
def matmul_compute(*idx_spatial):
k = te.reduce_axis((0, a_shape[-1]), name="k")
def multiply_compute(idx_reduce):
a_indices = []
b_indices = []
for i in range(offset):
if is_a_larger:
a_indices.append(idx_spatial[i])
else:
b_indices.append(idx_spatial[i])
for i in range(offset, len(output_shape) - (2 - a_prepended - b_appended)):
a_dim = a_shape[i if is_a_larger else i - offset]
b_dim = b_shape[i if not is_a_larger else i - offset]
dim_equal = a_dim == b_dim
if not isinstance(dim_equal, tirx.IntImm) or dim_equal == 0:
a_dim_is_one = isinstance(a_dim, tirx.IntImm) and a_dim == 1
b_dim_is_one = isinstance(b_dim, tirx.IntImm) and b_dim == 1
a_indices.append(0 if a_dim_is_one else idx_spatial[i])
b_indices.append(0 if b_dim_is_one else idx_spatial[i])
else:
a_indices.append(idx_spatial[i])
b_indices.append(idx_spatial[i])
if not a_prepended:
a_indices.append(idx_spatial[-2 + b_appended])
a_indices.append(idx_reduce)
if not b_appended:
b_indices.append(idx_spatial[-1])
b_indices.append(idx_reduce)
dtype = out_dtype
if dtype != "":
return a(*a_indices).astype(dtype) * b(*b_indices).astype(dtype)
return a(*a_indices) * b(*b_indices)
return te.sum(multiply_compute(k), axis=k)
return te.compute(
output_shape,
lambda *idx: matmul_compute(*idx), # pylint: disable=unnecessary-lambda
name="NT_matmul",
)
if isinstance(call.op, relax.GlobalVar):
function = self.builder_.get()[call.op]
if (
"Composite" in function.attrs
and function.attrs["Composite"] == "transpose_matmul_fuse"
):
out_dtype = function.ret_ty.dtype
return self.builder_.call_te(
te_transposed_matmul,
call.args[1],
call.args[0],
primfunc_name_hint="NT_matmul",
)
return super().visit_call_(call)
@@ -0,0 +1,148 @@
# 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.
"""Rewrite all-reduce operation to customized all-reduce impl with IPC memory.
The pass is written in Python for experiment, fast development.
"""
import tvm
from tvm import relax
from tvm.ir.module import IRModule
from tvm.relax.expr import Expr, Var
from tvm.relax.expr_functor import PyExprMutator, PyExprVisitor, mutator, visitor
@tvm.transform.module_pass(opt_level=0, name="IPCAllReduceRewrite")
class IPCAllReduceRewrite:
"""Rewrite all-reduce operation to customized all-reduce impl with IPC memory."""
def __init__(self, allreduce_strategy: int) -> None:
"""Constructor
Parameters
----------
allreduce_strategy : int
The all-reduce strategy. Only "1" and "2" are supported.
"1" stands for one-shot, and "2" stands for two-shot.
"""
self.allreduce_strategy = allreduce_strategy
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
fcustom_allreduce = tvm.get_global_func(
"runtime.disco.cuda_ipc.custom_allreduce", allow_missing=True
)
if fcustom_allreduce is None:
# Customized allreduce is not available.
return mod
binding_replacement_map = _Visitor(self.allreduce_strategy).visit(mod)
return _Rewriter(mod, binding_replacement_map).transform()
@visitor
class _Visitor(PyExprVisitor): # pylint: disable=abstract-method
def __init__(self, allreduce_strategy: int) -> None:
self.allreduce_strategy = allreduce_strategy
self.alloc_map: dict[Var, relax.Call] = {}
self.binding_replacement_map: dict[relax.Expr, relax.Expr] = {}
self.builtin_alloc_tensor_op = tvm.ir.Op.get("relax.builtin.alloc_tensor")
self.reshape_op = tvm.ir.Op.get("relax.reshape")
def visit(self, mod: IRModule) -> dict[relax.Expr, relax.Expr]:
"""Entry point"""
for _, func in mod.functions_items():
if isinstance(func, relax.Function):
self.alloc_map.clear()
self.visit_expr(func)
return self.binding_replacement_map
def visit_var_binding_(self, binding: relax.VarBinding):
super().visit_var_binding_(binding)
if (
isinstance(binding.value, relax.Call)
and binding.value.op == self.builtin_alloc_tensor_op
):
self.alloc_map[binding.var] = binding.value
elif isinstance(binding.value, relax.Var) and binding.value in self.alloc_map:
self.alloc_map[binding.var] = self.alloc_map[binding.value]
elif (
isinstance(binding.value, relax.Call)
and binding.value.op == self.reshape_op
and binding.value.args[0] in self.alloc_map
):
self.alloc_map[binding.var] = self.alloc_map[binding.value.args[0]]
def visit_call_(self, call: relax.Call) -> None: # pylint: disable=arguments-renamed
if (
not isinstance(call.op, relax.ExternFunc)
or call.op.global_symbol != "runtime.disco.allreduce"
or call.args[1].values[0] != 0
):
# Return if the call is not a summation all-reduce.
return
assert len(call.args) == 4
allreduce_input, _strategy, _ingroup, allreduce_output = call.args
alloc_tensor = self.alloc_map.get(allreduce_input, None)
if alloc_tensor is None or alloc_tensor.args[3].value != "global":
# Return if the allocation of all-reduce input is not recorded,
# or the scope of the allocation is not global.
return
# Set the scope of the alloc_tensor to IPC memory.
alloc_tensor = self.alloc_map[allreduce_input]
self.binding_replacement_map[alloc_tensor] = relax.op.builtin.alloc_tensor(
alloc_tensor.args[0],
alloc_tensor.args[1],
alloc_tensor.args[2],
relax.StringImm("ipc_memory"),
)
self.binding_replacement_map[call] = relax.Call(
relax.ExternFunc("runtime.disco.cuda_ipc.custom_allreduce"),
# The "cuda_ipc.custom_allreduce" implementation does not
# yet support num_groups>1, and therefore does not use the
# `in_group` argument.
[allreduce_input, relax.prim_value(self.allreduce_strategy), allreduce_output],
)
@mutator
class _Rewriter(PyExprMutator):
"""Rewrite the IRModule according to the binding replacement provided by the visitor."""
def __init__(
self, mod: IRModule, binding_replacement_map: dict[relax.Expr, relax.Expr]
) -> None:
super().__init__(mod)
self.mod = mod
self.binding_replacement_map = binding_replacement_map
def transform(self) -> IRModule:
"""Entry point"""
for g_var, func in self.mod.functions_items():
if isinstance(func, relax.Function):
updated_func = self.visit_expr(func)
self.builder_.update_func(g_var, updated_func)
return self.builder_.get()
def visit_call_(self, call: relax.Call) -> Expr: # pylint: disable=arguments-renamed
return (
super().visit_call_(self.binding_replacement_map[call])
if call in self.binding_replacement_map
else super().visit_call_(call)
)
@@ -0,0 +1,399 @@
# 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, unused-argument, missing-function-docstring, abstract-method, missing-class-docstring
# ruff: noqa: RUF005
"""Relax LazyTransformParams pass."""
import tvm
from tvm import IRModule, relax
from tvm.relax.expr_functor import PyExprMutator, PyExprVisitor, mutator, visitor
@visitor
class ForwardCollector(PyExprVisitor):
"""
Perform a forward pass to collect the following information:
out_tuple_map: map from var to its index in the output tuple
var_tuple_get_item: list of var that is bound to v = params[i]
Parameters
----------
tuple_var: relax.Var
The output tuple var
input_params: relax.Var
The input tuple var
"""
def __init__(self, tuple_var: relax.Var, input_params: relax.Var) -> None:
self.out_tuple_map = {}
self.out_tuple_var = tuple_var
self.input_params = input_params
self.var_tuple_get_item = []
self.is_tuple_get_item_input = False
def visit_tuple_getitem_(self, op: relax.TupleGetItem) -> None:
if op.tuple_value == self.input_params:
self.is_tuple_get_item_input = True
else:
self.is_tuple_get_item_input = False
super().visit_tuple_getitem_(op)
def visit_var_binding_(self, binding: relax.VarBinding) -> None:
if binding.var == self.out_tuple_var:
assert isinstance(binding.value, relax.Tuple)
for i, expr in enumerate(binding.value.fields):
if expr not in self.out_tuple_map:
self.out_tuple_map[expr] = []
self.out_tuple_map[expr].append(relax.prim_value(i))
else:
self.is_tuple_get_item_input = False
super().visit_var_binding_(binding)
if self.is_tuple_get_item_input:
self.var_tuple_get_item.append(binding.var)
@visitor
class LivenessAnalysis(PyExprVisitor):
"""
Perform a backward pass to collect the following information:
var_liveness_end: map from var to the list of var whose liveness is killed by this var binding
Parameters
----------
out_tuple_var: relax.Var
The output tuple var
input_params: set
The set of vars that are bound to v = params[i]
"""
def __init__(self, out_tuple_var: relax.Var) -> None:
self.last_appear_in_var_binding = []
self.out_tuple_var = out_tuple_var
self.var_liveness_end = {}
self.ended_vars = set()
def visit_binding_block_(self, block: relax.BindingBlock) -> None:
for binding in reversed(block.bindings):
self.visit_binding(binding)
def visit_var_(self, op: relax.Var) -> None:
if op not in self.ended_vars:
self.last_appear_in_var_binding.append(op)
self.ended_vars.add(op)
def visit_var_binding_(self, binding: relax.VarBinding) -> None:
if self.out_tuple_var == binding.var:
return
self.last_appear_in_var_binding = []
super().visit_var_binding_(binding)
# param[i] is in output
if binding.var not in self.ended_vars:
self.last_appear_in_var_binding.append(binding.var)
self.ended_vars.add(binding.var)
self.var_liveness_end[binding.var] = self.last_appear_in_var_binding
class LazyTransformParamsFuncCreator:
"""
Transform transform_params functions into a lazy version.
Parameters
----------
mod: IRModule
The module to be transformed
"""
def __init__(
self,
fget_item,
fset_item,
extra_get_item_params,
extra_set_item_params,
mod: IRModule = None,
) -> None:
self.mod = mod
self.fget_item = fget_item
self.extra_get_item_params = extra_get_item_params
self.fset_item = fset_item
self.extra_set_item_params = extra_set_item_params
self.input_params_set = None
self.out_tuple_map = None
self.out_tuple_var = None
self.memory_free_insertion = None
def transform(self, func: relax.Function) -> relax.Function:
if "num_input" in func.attrs:
num_input = func.attrs["num_input"]
else:
num_input = 0
seq_expr = func.body
self.out_tuple_var = seq_expr.body
# Step 1. collect out_tuple_map and input_params_set
forward_collector = ForwardCollector(self.out_tuple_var, func.params[num_input])
forward_collector.visit_expr(func)
self.out_tuple_map = forward_collector.out_tuple_map
# input_params_set is the set of binding var for var = params[i]
self.input_params_set = set(forward_collector.var_tuple_get_item)
# Step 2. liveness analysis and get where to insert kill_object instruction
liveness = LivenessAnalysis(self.out_tuple_var)
liveness.visit_expr(func)
self.memory_free_insertion = liveness.var_liveness_end
# Step 3. rewrite get item and set item
if self.fget_item is not None:
new_func = LazyInputMutator(self, self.mod).visit_expr(func)
new_body = new_func.body
if self.fset_item is not None:
# The LazyOutputMutator only inspects variable bindings
# for replacement. If the output tuple includes elements
# that do not have a variable binding, such as
# `relax.Const`, these must still produce a call to the
# `"set_item"` function.
leaf_outputs = {
expr: indices
for expr, indices in self.out_tuple_map.items()
if not isinstance(expr, relax.Var)
}
if leaf_outputs:
new_bindings = [
relax.VarBinding(
relax.Var("_", relax.AnyType()),
relax.Call(
relax.ExternFunc(self.fset_item),
[*self.extra_set_item_params, index, expr],
None,
[relax.AnyType()],
),
)
for expr, indices in leaf_outputs.items()
for index in indices
]
new_body = relax.SeqExpr(
[*new_body.blocks, relax.BindingBlock(new_bindings)], new_body.body
)
new_body = LazyOutputMutator(self, self.mod).visit_expr(new_body)
# Step 4. Add parameters of get_item and set_item (except index) to the function.
params = [
*func.params[:num_input],
*self.extra_get_item_params,
*self.extra_set_item_params,
]
# Step 5. Find all shape parameters that should be retained as
# parameters.
symbolic_vars = relax.analysis.defined_symbolic_vars(func)
if symbolic_vars:
def unpack_ty(ty):
if isinstance(ty, relax.TupleType):
for field in ty.fields:
yield from unpack_ty(field)
else:
yield ty
# direct iterate over the type annotation
for param in func.params[num_input:]:
for ty in unpack_ty(param.ty):
if isinstance(ty, tvm.ir.PrimType | relax.ShapeType):
params.append(relax.Var("symbolic_var_holder", ty))
return relax.Function(
params,
new_body,
relax.AnyType(),
attrs=func.attrs,
is_pure=False,
).without_attr("relax.force_pure")
@mutator
class LazyInputMutator(PyExprMutator):
def __init__(self, func_creator, mod: IRModule | None = None) -> None:
self.func_creator = func_creator
super().__init__(mod)
def visit_function_(self, func: relax.Function) -> relax.Expr:
if "num_input" in func.attrs:
num_input = func.attrs["num_input"]
else:
num_input = 0
params = list(func.params)[num_input:]
if len(params) == 1 and isinstance(params[0].ty, relax.TupleType):
self.tuple_param = params[0]
self.params = {}
else:
self.tuple_param = None
self.params = {var: i for i, var in enumerate(params)}
func = relax.Function(
func.params[:num_input],
func.body,
func.ret_ty,
is_pure=False,
attrs=func.attrs,
span=func.span,
).without_attr("relax.force_pure")
output = super().visit_function_(func)
self.tuple_param = None
self.params = {}
return output
def visit_var_(self, var: relax.Var) -> relax.Expr:
if var in self.params:
index = self.params[var]
get_item_result = self.builder_.emit(
relax.Call(
relax.ExternFunc(self.func_creator.fget_item),
self.func_creator.extra_get_item_params + [relax.prim_value(index)],
None,
[relax.AnyType()],
)
)
match_cast = relax.MatchCast(var, get_item_result, var.ty)
self.builder_.emit_normalized(match_cast)
del self.params[var]
return super().visit_var_(var)
def visit_tuple_getitem_(self, node: relax.TupleGetItem) -> relax.Expr:
ty = node.ty
node = super().visit_tuple_getitem_(node)
if self.tuple_param is not None and node.tuple_value.same_as(self.tuple_param):
get_item_result = self.builder_.emit(
relax.Call(
relax.ExternFunc(self.func_creator.fget_item),
self.func_creator.extra_get_item_params + [relax.prim_value(node.index)],
None,
[relax.AnyType()],
)
)
return self.builder_.match_cast(get_item_result, ty)
else:
return node
@mutator
class LazyOutputMutator(PyExprMutator):
def __init__(self, func_creator, mod: IRModule | None = None) -> None:
self.func_creator = func_creator
self.killed_vars = set()
super().__init__(mod)
def visit_var_(self, var: relax.Var) -> None:
assert var not in self.killed_vars
return super().visit_var_(var)
def visit_var_binding_(self, binding: relax.VarBinding) -> None:
if binding.var == self.func_creator.out_tuple_var:
# The function after rewriting returns a empty tuple.
func_output = self.builder_.emit(relax.Tuple([]))
self.set_var_remap(binding.var, func_output)
return
super().visit_var_binding_(binding)
if binding.var in self.func_creator.memory_free_insertion:
for var in self.func_creator.memory_free_insertion[binding.var]:
if var in self.func_creator.out_tuple_map:
self.killed_vars.add(var)
for index in self.func_creator.out_tuple_map[var]:
# rewrite set item
self.builder_.emit(
relax.Call(
relax.ExternFunc(self.func_creator.fset_item),
self.func_creator.extra_set_item_params
+ [index, super().visit_var_(var)],
None,
[relax.AnyType()],
),
name_hint="_",
)
if var in self.func_creator.input_params_set:
self.builder_.emit(
relax.op.vm.kill_object(super().visit_var_(var)), name_hint="_"
)
@tvm.transform.module_pass(opt_level=0, name="LazyTransformParams")
class LazyTransformParams:
"""
Convert transform_params functions into a lazy version.
(Load the input to memory on demand, and immediately free it after the last use.)
Note: ToNonDataflow() and RemovePurityTracking() should be invoked before this pass.
Parameters
----------
fget_item: str
The name of the get_item function.
fset_item: str
The name of the set_item function.
extra_get_item_params: list of relax.Var
The parameters of the get_item function except index.
The given parameters will be placed before index.
For example, if extra_get_item_params is [param1, param2], then the pass will generate
call_packed(fget_item, [param1, param2, index])
extra_set_item_params: list of relax.Var
The parameters of the set_item function except index and value.
The given parameters will be placed before index and value.
For example, if extra_set_item_params is [param1, param2], then the pass will generate
call_packed(fset_item, [param1, param2, index, value])
"""
def __init__(
self,
fget_item="get_item",
fset_item="set_item",
extra_get_item_params=None,
extra_set_item_params=None,
) -> None:
self.fget_item = fget_item
self.extra_get_item_params = [] if extra_get_item_params is None else extra_get_item_params
assert self.fget_item is not None, "transforming set_item only is not supported"
self.fset_item = fset_item
self.extra_set_item_params = [] if extra_set_item_params is None else extra_set_item_params
def transform_module(self, mod: IRModule, ctx: tvm.transform.PassContext) -> IRModule:
lazy_mutator = LazyTransformParamsFuncCreator(
self.fget_item,
self.fset_item,
self.extra_get_item_params,
self.extra_set_item_params,
mod,
)
builder = relax.BlockBuilder(mod)
for gv, _ in mod.functions_items():
if gv.name_hint.endswith("transform_params"):
func = mod[gv]
if not isinstance(func, relax.Function):
continue
func = lazy_mutator.transform(func)
builder.update_func(gv, func)
return builder.get()
@@ -0,0 +1,39 @@
# isort: skip_file
# 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.
"""Legalize high-level operator calls in Relax functions to call_tir."""
from . import binary
from . import ccl
from . import create
from . import datatype
from . import distributed
from . import grad
from . import image
from . import index
from . import inspect_op
from . import linear_algebra
from . import manipulate
from . import nn
from . import qdq
from . import search
from . import statistical
from . import unary
from . import vision
# Device specific legalizations
from . import adreno
@@ -0,0 +1,20 @@
# isort: skip_file
# 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.
"""Legalize high-level operator calls in Relax functions to call_tir."""
from .convolution import conv2d_NCHWc_OIHWo
@@ -0,0 +1,36 @@
# 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=missing-docstring, invalid-name
"""A Convolution impl for Adreno GPU."""
from tvm import relax, topi
def conv2d_NCHWc_OIHWo(bb: relax.BlockBuilder, call: relax.Call) -> relax.Expr:
return bb.call_te(
topi.nn.conv2d_NCHWc_OIHWo,
data=call.args[0],
kernel=call.args[1],
stride=call.attrs.strides,
padding=call.attrs.padding,
dilation=call.attrs.dilation,
layout=call.attrs.data_layout,
out_layout=call.attrs.out_layout,
# out_dtype=call.attrs.out_dtype,
ty_args=call.ty_args,
primfunc_name_hint="conv2d_NCHWc_OIHWo",
)
@@ -0,0 +1,83 @@
# 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 binary operators."""
from tvm import topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import (
LegalizeFunc,
TEFunc,
_is_relax_expr,
_try_convert_to_scalar_const,
register_legalize,
)
def _binary(te_func: TEFunc) -> LegalizeFunc:
"""A common wrapper util for the legalization of binary operators.
It detects if one of the binary op arguments is a constant scalar. It so,
it extracts the scalar value to simplify the generated PrimFunc.
"""
def binary_call_te(bb: BlockBuilder, call: Call) -> Expr:
# To simplify the created PrimFunc, we first check if arg1 is a constant scalar.
# If it is not, we then check if arg0 is a constant scalar.
arg0 = call.args[0]
arg1 = _try_convert_to_scalar_const(call.args[1])
if _is_relax_expr(arg1):
arg0 = _try_convert_to_scalar_const(arg0)
return bb.call_te(te_func, arg0, arg1)
return binary_call_te
register_legalize("relax.add", _binary(topi.add))
register_legalize("relax.divide", _binary(topi.divide))
register_legalize("relax.floor_divide", _binary(topi.floor_divide))
register_legalize("relax.log_add_exp", _binary(topi.log_add_exp))
register_legalize("relax.multiply", _binary(topi.multiply))
register_legalize("relax.power", _binary(topi.power))
register_legalize("relax.atan2", _binary(topi.atan2))
register_legalize("relax.subtract", _binary(topi.subtract))
register_legalize("relax.equal", _binary(topi.equal))
register_legalize("relax.mod", _binary(topi.mod))
register_legalize("relax.floor_mod", _binary(topi.floor_mod))
register_legalize("relax.greater", _binary(topi.greater))
register_legalize("relax.greater_equal", _binary(topi.greater_equal))
register_legalize("relax.less", _binary(topi.less))
register_legalize("relax.less_equal", _binary(topi.less_equal))
register_legalize("relax.not_equal", _binary(topi.not_equal))
register_legalize("relax.maximum", _binary(topi.maximum))
register_legalize("relax.minimum", _binary(topi.minimum))
# bitwise
register_legalize("relax.bitwise_and", _binary(topi.bitwise_and))
register_legalize("relax.bitwise_or", _binary(topi.bitwise_or))
register_legalize("relax.bitwise_xor", _binary(topi.bitwise_xor))
register_legalize("relax.left_shift", _binary(topi.left_shift))
register_legalize("relax.right_shift", _binary(topi.right_shift))
# logical
register_legalize("relax.logical_and", _binary(topi.logical_and))
register_legalize("relax.logical_or", _binary(topi.logical_or))
register_legalize("relax.logical_xor", _binary(topi.logical_xor))
@@ -0,0 +1,125 @@
# 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 ccl operators."""
from tvm import arith, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr, ShapeExpr
from ...op import call_dps_packed
from ...type import ShapeType, TensorType
from .common import register_legalize
@register_legalize("relax.ccl.allreduce")
def _allreduce(_bb: BlockBuilder, call: Call) -> Expr:
op_type_str = call.attrs.op_type
op_type_map = {
"sum": 0,
"prod": 1,
"min": 2,
"max": 3,
"avg": 4,
}
if op_type_str not in op_type_map:
raise ValueError(
f"Unsupported reduction operation: {op_type_str}. "
f"Supported operations are {op_type_map.keys()}."
)
return call_dps_packed(
"runtime.disco.allreduce",
[call.args[0], ShapeExpr([op_type_map[op_type_str]]), call.attrs.in_group],
out_ty=call.args[0].ty,
)
@register_legalize("relax.ccl.allgather")
def _allgather(_bb: BlockBuilder, call: Call) -> Expr:
output_shape = []
arg_ty = call.args[0].ty
assert isinstance(arg_ty, TensorType), "The input type of allgather should be TensorType."
assert isinstance(arg_ty.shape.ty, ShapeType)
arg_shape = arg_ty.shape.ty
for i, shape_value in enumerate(arg_shape.values):
if i == 0:
output_shape.append(shape_value * call.attrs.num_workers)
else:
output_shape.append(shape_value)
return call_dps_packed(
"runtime.disco.allgather",
[call.args[0], call.attrs.in_group],
out_ty=TensorType(
shape=output_shape,
dtype=arg_ty.dtype,
vdevice=arg_ty.vdevice,
),
)
@register_legalize("relax.ccl.broadcast_from_worker0")
def _broadcast_from_worker0(_bb: BlockBuilder, call: Call) -> Expr:
return call_dps_packed(
"runtime.disco.broadcast_from_worker0",
[call.args[0], False],
out_ty=call.args[0].ty,
)
# Since collective communication ops are performed on contiguous memory,
# we need to reshape and transpose the input tensor to make sharding dimension in the highest order
def _transpose_for_ccl(_bb: BlockBuilder, expr: Expr, axis: int, num_workers: int):
assert isinstance(expr.ty, TensorType), "The input type should be TensorType."
assert isinstance(expr.ty.shape.ty, ShapeType)
arg_shape = expr.ty.shape.ty
new_shape = []
for i, shape_value in enumerate(arg_shape.values):
if i == axis:
modulo = arith.Analyzer().simplify(shape_value % num_workers)
assert modulo == 0, (
f"scatter_from_worker0 expects the size of axis {axis} of input tensor "
"to be divisible by num_workers. However, the axis 0 of input tensor "
f"is {shape_value} while num_workers is {num_workers}"
)
new_shape.append(num_workers)
new_shape.append(tirx.div(shape_value, num_workers))
else:
new_shape.append(shape_value)
reshape_var = _bb.emit_te(topi.reshape, expr, new_shape)
if axis == 0:
return reshape_var
permute_order = [axis] + list(range(axis)) + list(range(axis + 1, len(new_shape)))
transpose_var = _bb.emit_te(topi.transpose, reshape_var, permute_order)
return transpose_var
@register_legalize("relax.ccl.scatter_from_worker0")
def _scatter_from_worker0(_bb: BlockBuilder, call: Call) -> Expr:
transpose_var = _transpose_for_ccl(_bb, call.args[0], call.attrs.axis, call.attrs.num_workers)
output_shape = transpose_var.ty.shape.ty.values
output_shape = output_shape[1:]
return call_dps_packed(
"runtime.disco.scatter_from_worker0",
[transpose_var, False],
out_ty=TensorType(
shape=output_shape,
dtype=call.args[0].ty.dtype,
vdevice=call.args[0].ty.vdevice,
),
)
@@ -0,0 +1,125 @@
# 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.
"""Common functionality for legalization."""
from collections.abc import Callable
import tvm
from tvm import te
from tvm.ir import Call
from tvm.runtime import DataTypeCode
from tvm.tirx import FloatImm, IntImm
from ...block_builder import BlockBuilder
from ...expr import Constant, Expr
##################### Types #####################
# The function type of a TE function, which accepts TE Tensors and
# other attributes, and returns the output TE Tensor.
TEFunc = Callable[..., te.Tensor]
# The function type of a legalization function, which takes a
# BlockBuilder and the Call to be legalized, and outputs the legalization
# result Expr.
LegalizeFunc = Callable[[BlockBuilder, Call], Expr]
def _is_relax_expr(expr: object) -> bool:
return isinstance(expr, Expr) and not tvm.ir.is_prim_expr(expr)
def _try_convert_to_scalar_const(
expr: Expr, python_native: bool = False
) -> Expr | FloatImm | IntImm | bool | float | int:
"""Check if the input Expr is a scalar constant.
If it is, return its plain value with the same data type or in native python type.
If it is not, return the input expr.
Note that if the python_native flag is True, the returned value will be in native python type,
this might cause loss of data type for example, a float16 constant will be converted to float32
and a int64 constant will be converted to int32.
Parameters
----------
expr : Expr
The expr to be checked and converted.
Returns
-------
ret : Union[Expr, FloatImm, IntImm, bool, float, int]
Return a FloatImm or IntImm if the given expr is a scalar integer or float constant, and the
python native flag is False. Or return the plain value of the constant in native python type
if the python native flag is True.
Or return the input itself if it is not a scalar constant.
"""
if isinstance(expr, Constant) and expr.ty.ndim == 0:
# get the value of the scalar constant
value = expr.data.numpy()[()].item()
dtype = expr.ty.dtype
dtype_str = str(dtype.dtype)
if python_native:
return value
# preserve the data type of the constant
if dtype.matches_code(DataTypeCode.FLOAT, DataTypeCode.BFLOAT):
return tvm.tirx.FloatImm(dtype_str, value)
elif dtype.matches_code(DataTypeCode.INT, DataTypeCode.UINT, DataTypeCode.BOOL):
return tvm.tirx.IntImm(dtype_str, value)
return expr
def _call_topi_without_attr(te_func: TEFunc, primfunc_name: str | None = None) -> LegalizeFunc:
"""A common wrapper util for the ops who has no attributes and whose
legalization is simply passing its arguments to some TE function.
Parameters
----------
te_func : TEFunc
The input TE function which is to be converted to PrimFunc.
primfunc_name : Optional[str]
The name of the generated PrimFunc.
If it is not specified, the name of `te_func` will be used by default.
Returns
-------
func : LegalizeFunc
The legalization wrapper function, which wraps the input TE function.
"""
if primfunc_name is None:
primfunc_name = te_func.__name__
return lambda bb, call: bb.call_te(te_func, *call.args, primfunc_name_hint=primfunc_name)
##################### Decorators #####################
_LEGALIZE_ATTR_NAME = "FLegalize"
def register_legalize(op_name: str, legal_func: LegalizeFunc = None):
"""Register legal transformation function for a Relax op.
Parameters
----------
op_name : str
The name of the operator
legal_func: function (bb: BlockBuilder, call: Call) -> new_expr: Expr
The function for transforming an expr to another expr.
"""
return tvm.ir.register_op_attr(op_name, _LEGALIZE_ATTR_NAME, legal_func)
@@ -0,0 +1,166 @@
# 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: E731
"""Default legalization function for creation operators."""
import numpy as np
import tvm
from tvm import te, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr, ShapeExpr, const
from ...type import ShapeType
from .common import LegalizeFunc, _try_convert_to_scalar_const, register_legalize
def _full(is_like: bool, fill_value: float | None, primfunc_name: str) -> LegalizeFunc:
def full_call_te(bb: BlockBuilder, call: Call) -> Expr:
_fill_value = (
_try_convert_to_scalar_const(call.args[1], python_native=True)
if fill_value is None
else fill_value
)
shape = call.args[0].ty.shape if is_like else call.args[0]
if isinstance(shape, ShapeExpr):
output_shape = shape.values
else:
assert isinstance(shape.ty, ShapeType)
assert shape.ty.ndim >= 0
shape = bb.emit(shape)
output_shape = [tirx.Var(f"s{i}", "int64") for i in range(shape.ty.ndim)]
bb.match_cast(shape, ShapeType(output_shape))
return bb.call_te(
topi.full,
output_shape,
call.ty.dtype,
_fill_value,
primfunc_name_hint=primfunc_name,
)
return full_call_te
def _tril_triu(is_upper: bool, primfunc_name: str) -> LegalizeFunc:
def tril_triu_call_te(bb: BlockBuilder, call: Call) -> Expr:
data, k = call.args
return bb.call_te(
topi.trilu,
data,
k,
upper=is_upper,
primfunc_name_hint=primfunc_name,
)
return tril_triu_call_te
register_legalize("relax.full", _full(is_like=False, fill_value=None, primfunc_name="full"))
register_legalize("relax.full_like", _full(is_like=True, fill_value=None, primfunc_name="full"))
register_legalize("relax.ones", _full(is_like=False, fill_value=1.0, primfunc_name="ones"))
register_legalize("relax.ones_like", _full(is_like=True, fill_value=1.0, primfunc_name="ones"))
register_legalize("relax.zeros", _full(is_like=False, fill_value=0.0, primfunc_name="zeros"))
register_legalize("relax.zeros_like", _full(is_like=True, fill_value=0.0, primfunc_name="zeros"))
register_legalize("relax.tril", _tril_triu(is_upper=False, primfunc_name="tril"))
register_legalize("relax.triu", _tril_triu(is_upper=True, primfunc_name="triu"))
def _eye(is_like: bool, primfunc_name: str) -> LegalizeFunc:
def eye_call_te(bb: BlockBuilder, call: Call) -> Expr:
_convert_to_scalar_const = lambda x: _try_convert_to_scalar_const(x, python_native=True)
if is_like:
x = call.args[0]
k = _convert_to_scalar_const(call.args[1]) if len(call.args) > 1 else 0
n, m = x.ty.shape
dtype = x.ty.dtype
else:
n = _convert_to_scalar_const(call.args[0])
m = _convert_to_scalar_const(call.args[1]) if len(call.args) > 1 else n
k = _convert_to_scalar_const(call.args[2]) if len(call.args) > 2 else 0
dtype = call.attrs.dtype
return bb.call_te(
topi.eye,
n,
m,
k,
dtype,
primfunc_name_hint=primfunc_name,
)
return eye_call_te
register_legalize("relax.eye", _eye(is_like=False, primfunc_name="eye"))
register_legalize("relax.eye_like", _eye(is_like=True, primfunc_name="eye_like"))
@register_legalize("relax.arange")
def _arange(bb: BlockBuilder, call: Call) -> Expr:
assert len(call.args) == 3
assert all(tvm.ir.is_prim_expr(x) for x in call.args)
start, end, step = call.args
dtype = call.attrs.dtype
def is_const_scalar(x: tirx.Expr):
return isinstance(x, tirx.IntImm | tirx.FloatImm)
if all([is_const_scalar(x) for x in call.args]):
return const(np.arange(start.value, end.value, step.value, dtype=dtype), dtype=dtype)
else:
return bb.call_te(topi.arange, start, end, step, dtype)
@register_legalize("relax.shape_to_tensor")
def _shape_to_tensor(bb: BlockBuilder, call: Call) -> Expr:
shape = call.args[0]
values = shape.values if isinstance(shape, ShapeExpr) else shape.ty.values
if values is None:
return call
values = list(values)
n = len(values)
symbolic = [v for v in values if not isinstance(v, tirx.IntImm)]
def te_shape_to_tensor(*sym):
sym = list(sym)
resolved = [v if isinstance(v, tirx.IntImm) else sym.pop(0) for v in values]
def fcompute(i):
result = tirx.const(0, "int64")
for idx in range(n - 1, -1, -1):
result = tirx.if_then_else(i == idx, tirx.Cast("int64", resolved[idx]), result)
return result
return te.compute((n,), fcompute, name="shape_to_tensor")
return bb.call_te(te_shape_to_tensor, *symbolic, primfunc_name_hint="shape_to_tensor")
@register_legalize("relax.hamming_window")
def _hamming_window(bb: BlockBuilder, call: Call) -> Expr:
assert len(call.args) == 4
dtype = call.attrs.dtype
window_size = call.args[0]
periodic = call.args[1]
alpha = call.args[2]
beta = call.args[3]
return bb.call_te(topi.hamming_window, window_size, periodic, alpha, beta, dtype)
@@ -0,0 +1,34 @@
# 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 datatype operators."""
from tvm import relax, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import _is_relax_expr, _try_convert_to_scalar_const, register_legalize
@register_legalize("relax.astype")
def _astype(bb: BlockBuilder, call: Call) -> Expr:
arg = _try_convert_to_scalar_const(call.args[0], python_native=True)
if _is_relax_expr(arg):
return bb.call_te(topi.cast, arg, call.attrs.dtype)
else:
return relax.const(arg, call.attrs.dtype)
@@ -0,0 +1,45 @@
# 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 distir-related operators."""
from tvm import relax, tirx
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from ...op import call_pure_packed
from ...type import ShapeType
from .common import register_legalize
@register_legalize("relax.dist.redistribute_replica_to_shard")
def _redistribute_replica_to_shard(_bb: BlockBuilder, call: Call) -> Expr:
num_workers = call.attrs.num_workers
axis = call.attrs.axis
worker_id_symbol = tirx.Var("worker_id", "int64")
worker_id_var = _bb.emit(call_pure_packed("runtime.disco.worker_id", ty_args=[ShapeType(None)]))
_bb.match_cast(worker_id_var, ShapeType([worker_id_symbol]))
split_axis_size = call.args[0].ty.shape[axis]
return relax.op.strided_slice(
call.args[0],
axes=[axis],
begin=[worker_id_symbol * split_axis_size // num_workers],
end=[(worker_id_symbol + 1) * split_axis_size // num_workers],
assume_inbound=True,
)
@@ -0,0 +1,241 @@
# 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,unused-argument
"""Default legalization function for perators to implement operaor gradients."""
import logging
from tvm import te, tirx, topi
from tvm.ir import Call
from tvm.script.ir_builder import IRBuilder
from tvm.script.ir_builder import tirx as T
from tvm.tirx.script.builder.utils import buffer_proxy
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import register_legalize
@register_legalize("relax.grad.no_grad")
def _no_grad(bb: BlockBuilder, call: Call) -> Expr:
return call.args[0]
@register_legalize("relax.grad.start_checkpoint")
def _start_checkpoint(bb: BlockBuilder, call: Call) -> Expr:
return call.args[0]
@register_legalize("relax.grad.end_checkpoint")
def _end_checkpoint(bb: BlockBuilder, call: Call) -> Expr:
return call.args[0]
@register_legalize("relax.grad.nll_loss_backward")
def _grad_nll_loss_backward(bb: BlockBuilder, call: Call) -> Expr:
# topi.sum don't support zero-dim x
# we add support for that
def topi_sum_extend(x):
return x if x.ndim == 0 else topi.sum(x)
def te_nll_loss_backward(output_grad, predictions, targets, weights, reduction, ignore_index):
# handle ignore_index
if ignore_index >= 0:
weights = te.compute(
weights.shape,
lambda i: tirx.Select(i == ignore_index, tirx.const(0, weights.dtype), weights(i)),
"weights_new",
)
all_weights = te.compute(targets.shape, lambda *i: weights(targets(*i)), "all_weights")
# handle reduction
if reduction == "sum":
output_grad = topi.broadcast_to(output_grad, targets.shape)
elif reduction == "mean":
weight_sum = topi_sum_extend(all_weights)
output_grad = topi.divide(topi.broadcast_to(output_grad, targets.shape), weight_sum)
# handle no batch
if predictions.ndim == 1:
return te.compute(
predictions.shape,
lambda i: tirx.Select(
i == targets(), -all_weights() * output_grad(), tirx.const(0, predictions.dtype)
),
"pred_grad",
)
return te.compute(
predictions.shape,
lambda *i: tirx.Select(
i[1] == targets(*i[:1], *i[2:]),
-all_weights(*i[:1], *i[2:]) * output_grad(*i[:1], *i[2:]),
tirx.const(0, predictions.dtype),
),
"pred_grad",
)
def te_nll_loss_backward_no_weight(output_grad, predictions, targets, reduction, ignore_index):
weight = topi.full(
(predictions.shape[1] if len(predictions.shape) > 1 else predictions.shape[0],),
predictions.dtype,
1.0,
)
return te_nll_loss_backward(
output_grad, predictions, targets, weight, reduction, ignore_index
)
if len(call.args) == 3:
return bb.call_te(
te_nll_loss_backward_no_weight,
*call.args,
reduction=call.attrs.reduction,
ignore_index=call.attrs.ignore_index,
)
return bb.call_te(
te_nll_loss_backward,
*call.args,
reduction=call.attrs.reduction,
ignore_index=call.attrs.ignore_index,
primfunc_name_hint="nll_loss_backward",
)
@register_legalize("relax.grad.max_pool2d_backward")
def _grad_max_pool2d_backward(bb: BlockBuilder, call: Call) -> Expr:
if not (len(call.attrs.dilation) == 2 and all(i == 1 for i in call.attrs.dilation)):
logging.info("Dilation is not supported in TOPI pool_grad and is not legalized.")
return call
return bb.call_te(
topi.nn.pool_grad,
call.args[0],
call.args[1],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
padding=call.attrs.padding,
pool_type="max",
ceil_mode=call.attrs.ceil_mode,
count_include_pad=call.attrs.count_include_pad,
layout=call.attrs.layout,
primfunc_name_hint="max_pool2d_backward",
)
@register_legalize("relax.grad.avg_pool2d_backward")
def _grad_avg_pool2d_backward(bb: BlockBuilder, call: Call) -> Expr:
if not (len(call.attrs.dilation) == 2 and all(i == 1 for i in call.attrs.dilation)):
logging.info("Dilation is not supported in TOPI pool_grad and is not legalized.")
return call
return bb.call_te(
topi.nn.pool_grad,
call.args[0],
call.args[1],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
padding=call.attrs.padding,
pool_type="avg",
ceil_mode=call.attrs.ceil_mode,
count_include_pad=call.attrs.count_include_pad,
layout=call.attrs.layout,
primfunc_name_hint="avg_pool2d_backward",
)
@register_legalize("relax.grad.take_backward")
def _grad_take_backward(bb: BlockBuilder, call: Call) -> Expr:
axis = call.attrs.axis
if axis is not None:
axis = int(axis)
def te_take_backward(output_grad, x, indices):
def gen_ir(output_grad_ptr, x_ptr, indices_ptr, out_ptr):
# pylint: disable=invalid-name
# Use buffer_proxy for flat indexing on multi-dimensional buffers
out = buffer_proxy(out_ptr)
grad = buffer_proxy(output_grad_ptr)
idx = buffer_proxy(indices_ptr)
fused_shape = 1
for i in x_ptr.shape:
fused_shape *= i
assert len(indices_ptr.shape) == 1 # indices in take must be 1-dim Tensor
indices_len = indices_ptr.shape[0]
with IRBuilder() as ib:
with T.seq_scope():
# Init loop (zero-fill output buffer)
with T.serial(fused_shape) as i:
out[i] = tirx.const(0, dtype=x_ptr.dtype)
# Accumulation loop
if axis is not None:
fused_output_grad_shape_pre = 1
fused_output_grad_shape_nxt = 1
for i in range(len(output_grad_ptr.shape)):
if i < axis:
fused_output_grad_shape_pre *= output_grad_ptr.shape[i]
elif i > axis:
fused_output_grad_shape_nxt *= output_grad_ptr.shape[i]
x_axis_len = x_ptr.shape[axis]
with T.serial(
fused_output_grad_shape_pre * fused_output_grad_shape_nxt
) as fused:
i = fused // fused_output_grad_shape_nxt
j = fused % fused_output_grad_shape_nxt
with T.serial(indices_len) as loop_l:
out_idx = (
i * fused_output_grad_shape_nxt * x_axis_len
+ idx[loop_l] * fused_output_grad_shape_nxt
+ j
)
grad_idx = (
i * fused_output_grad_shape_nxt * indices_len
+ loop_l * fused_output_grad_shape_nxt
+ j
)
out[out_idx] = out[out_idx] + grad[grad_idx]
else:
with T.serial(indices_len) as loop_l:
out[idx[loop_l]] = out[idx[loop_l]] + grad[loop_l]
return ib.get()
shape = x.shape
out_buf = tirx.decl_buffer(shape, x.dtype, "out_buf", layout=None)
return te.extern(
[shape],
[output_grad, x, indices],
lambda ins, outs: gen_ir(ins[0], ins[1], ins[2], outs[0]),
dtype=x.dtype,
out_buffers=[out_buf],
name="take_backward",
tag="take_backward",
)
return bb.call_te(
te_take_backward,
call.args[0],
call.args[1],
call.args[2],
primfunc_name_hint="take_backward",
)
@@ -0,0 +1,88 @@
# 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 image operators."""
from tvm import tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import register_legalize
@register_legalize("relax.image.resize2d")
def _image_resize2d(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.image.resize2d,
call.args[0],
roi=call.attrs.roi,
size=call.args[1],
layout=call.attrs.layout,
method=call.attrs.method,
coordinate_transformation_mode=call.attrs.coordinate_transformation_mode,
rounding_method=call.attrs.rounding_method,
bicubic_alpha=call.attrs.cubic_alpha,
bicubic_exclude=call.attrs.cubic_exclude,
extrapolation_value=call.attrs.extrapolation_value,
)
@register_legalize("relax.image.grid_sample")
def _image_grid_sample(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.image.grid_sample,
call.args[0],
call.args[1],
method=call.attrs.method,
layout=call.attrs.layout,
padding_mode=call.attrs.padding_mode,
align_corners=call.attrs.align_corners,
)
@register_legalize("relax.image.affine_grid")
def _image_affine_grid(bb: BlockBuilder, call: Call) -> Expr:
for v in call.args[1].values:
if not isinstance(v, int | tirx.IntImm):
raise ValueError(
f"affine_grid legalization requires static target_shape, got symbolic value: {v}"
)
target_shape = [int(v) for v in call.args[1].values]
return bb.call_te(
topi.image.affine_grid,
call.args[0],
target_shape=target_shape,
align_corners=call.attrs.align_corners,
)
@register_legalize("relax.image.resize3d")
def _image_resize3d(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.image.resize3d,
call.args[0],
roi=call.attrs.roi,
size=call.args[1],
layout=call.attrs.layout,
method=call.attrs.method,
coordinate_transformation_mode=call.attrs.coordinate_transformation_mode,
rounding_method=call.attrs.rounding_method,
bicubic_alpha=call.attrs.cubic_alpha,
bicubic_exclude=call.attrs.cubic_exclude,
extrapolation_value=call.attrs.extrapolation_value,
)
@@ -0,0 +1,137 @@
# 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 index operators."""
import tvm
from tvm import te, tirx, topi
from tvm.ir import Call, PrimType
from ...block_builder import BlockBuilder
from ...expr import Expr, Tuple
from ...op import tensor_to_shape
from ...type import ShapeType
from .common import register_legalize
@register_legalize("relax.take")
def _take(bb: BlockBuilder, call: Call) -> Expr:
# Currently "fast" is the default mode, which leads to segmentation faults
# when there are out-of-bounds indices.
return bb.call_te(topi.take, call.args[0], call.args[1], call.attrs.axis, mode=call.attrs.mode)
@register_legalize("relax.strided_slice")
def _strided_slice(bb: BlockBuilder, call: Call) -> Expr:
def _relax_tuple_to_tir(relax_tuple):
if isinstance(relax_tuple, Tuple):
output = []
for field in relax_tuple.fields:
assert tvm.ir.is_prim_expr(field)
output.append(field)
return output
output = []
for field in relax_tuple.ty.fields:
assert isinstance(field, PrimType)
return None
return output
if len(call.args) == 4:
data, axes, begin, end = call.args
strides = [tirx.IntImm("int64", 1)] * len(axes.ty.fields)
elif len(call.args) == 5:
data, axes, begin, end, strides = call.args
strides = _relax_tuple_to_tir(strides)
else:
raise ValueError(
f"Expression {call} provides {len(call.args)} arguments, "
f"but {call.op} requires either 4 or 5 arguments."
)
axes = _relax_tuple_to_tir(axes)
begin = _relax_tuple_to_tir(begin)
end = _relax_tuple_to_tir(end)
return bb.call_te(
topi.strided_slice,
data,
begin,
end,
strides,
axes,
slice_mode="end",
assume_inbound=call.attrs.assume_inbound,
)
@register_legalize("relax.dynamic_strided_slice")
def _dynamic_strided_slice(bb: BlockBuilder, call: Call) -> Expr:
assert len(call.args) == 4
data, begin, end, strides = call.args
# 1. Insert shape function
def shape_func(data, begin, end, strides):
def _compute(i):
def canonicalize_index(index, extent, strides):
begin_range = tirx.Select(
strides < 0, tirx.const(-1, "int64"), tirx.const(0, "int64")
)
end_range = tirx.Select(strides < 0, extent - 1, extent)
index = tirx.Select(index < 0, index + extent, index)
return tirx.Min(tirx.Max(index, begin_range), end_range)
def get_length(begin, end, strides, length):
begin = canonicalize_index(begin, length, strides)
end = canonicalize_index(end, length, strides)
len1 = tirx.ceildiv(begin - end, -strides)
len2 = tirx.ceildiv(end - begin, strides)
return tirx.Select(strides < 0, len1, len2)
length = tirx.const(-1, "int64")
for idx in range(data.ndim):
length = tirx.Select(i == tirx.const(idx, "int64"), data.shape[idx], length)
return get_length(begin[i], end[i], strides[i], length)
return te.compute((begin.shape[0],), _compute, name="T_shape_func_strided_slice_dynamic")
output_shape = bb.normalize(
bb.call_te(
shape_func,
data,
begin,
end,
strides,
)
)
# 2. Convert tensor to shape and match cast with new symbolic vars
ndim = int(output_shape.ty.shape[0])
output_shape = bb.emit(tensor_to_shape(output_shape))
output_shape_vars = [tirx.Var("s", "int64") for i in range(ndim)]
bb.match_cast(output_shape, ShapeType(output_shape_vars))
# 3. Pass the output shape vars to TOPI
return bb.call_te(
topi.dynamic_strided_slice,
call.args[0],
call.args[1],
call.args[2],
call.args[3],
output_shape=output_shape_vars,
)
@@ -0,0 +1,137 @@
# 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
"""Legalization functions for DLTensor inspection."""
import enum
from tvm.ir import Call
from tvm.script import tirx as T
from ... import op
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import register_legalize
class TVMStructFieldKind(enum.IntEnum):
"""Equivalent to tvm::tirx::builtin::TVMStructFieldKind
This does not use `enum.auto()` to define the values, because
`enum.auto()` starts from 1, and this must match the C++
definition which starts from 0.
"""
kDLTensorAddr = 0
kDLTensorData = 1
kDLTensorShape = 2
kDLTensorStrides = 3
kDLTensorNDim = 4
kDLTensorTypeCode = 5
kDLTensorTypeBits = 6
kDLTensorTypeLanes = 7
kDLTensorByteOffset = 8
kDLTensorDeviceId = 9
kDLTensorDeviceType = 10
kDLTensorKindBound_ = 11
kTVMValueContent = 12
kTVMValueKindBound_ = 13
@register_legalize("relax.inspect.tensor_stride_i")
def _tensor_stride_i(bb: BlockBuilder, call: Call) -> Expr:
@T.prim_func(private=True, s_tir=True)
def _get_tensor_stride_i(dlpack_handle: T.handle, axis: T.int64) -> T.int64:
T.func_attr({"tirx.is_host_func": True, "tirx.is_scheduled": True})
assert T.int64(0) <= axis, "Specified axis may not be negative"
ndim: T.let[T.int32] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorNDim), "int32"
)
assert axis < T.Cast("int64", ndim), (
"Specified axis may not be larger than the tensor's dimensionality"
)
stride_ptr: T.let[T.handle("int64")] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorStrides), T.handle("int64").ty
)
if T.isnullptr(stride_ptr):
shape_ptr: T.let[T.handle("int64")] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorShape), T.handle("int64").ty
)
shape = T.decl_buffer(ndim, "int64", data=shape_ptr)
product = T.decl_buffer([], "int64")
product[()] = 1
# TODO(Lunderberg): Add a TIR lowering pass to allow
# ranges to start somewhere other than zero. This loop
# could then iterate on `range(axis+1, ndim)`.
for dim_offset in range(ndim - (axis + 1)):
dim: T.let[T.int64] = dim_offset + (axis + 1)
product[()] = product[()] * shape[dim]
return product[()]
else:
strides = T.decl_buffer(ndim, "int64", data=stride_ptr)
stride: T.let[T.int64] = strides[axis]
return stride
gvar = bb.add_func(_get_tensor_stride_i, "_get_tensor_stride_i")
return Call(gvar, call.args)
@register_legalize("relax.inspect.tensor_byte_offset")
def _tensor_byte_offset(bb: BlockBuilder, call: Call) -> Expr:
@T.prim_func(private=True, s_tir=True)
def _get_tensor_byte_offset(dlpack_handle: T.handle) -> T.int64:
T.func_attr({"tirx.is_host_func": True, "tirx.is_scheduled": True})
byte_offset: T.let[T.uint64] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorByteOffset), "uint64"
)
return byte_offset
gvar = bb.add_func(_get_tensor_byte_offset, "_get_tensor_byte_offset")
return Call(gvar, call.args)
@register_legalize("relax.inspect.tensor_elem_offset")
def _tensor_elem_offset(bb: BlockBuilder, call: Call) -> Expr:
@T.prim_func(private=True, s_tir=True)
def _get_tensor_elem_offset(dlpack_handle: T.handle) -> T.int64:
T.func_attr({"tirx.is_host_func": True, "tirx.is_scheduled": True})
byte_offset: T.let[T.uint64] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorByteOffset), "uint64"
)
scalar_bits: T.let[T.uint8] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorTypeBits), "uint8"
)
lanes: T.let[T.uint16] = T.tvm_struct_get(
dlpack_handle, 0, int(TVMStructFieldKind.kDLTensorTypeLanes), "uint16"
)
bytes_per_element: T.let[T.uint64] = T.ceildiv(
scalar_bits.astype("uint64") * lanes.astype("uint64"), 8
)
elem_offset: T.let[T.uint64] = byte_offset // bytes_per_element
return elem_offset
gvar = bb.add_func(_get_tensor_elem_offset, "_get_tensor_elem_offset")
return Call(gvar, call.args)
@register_legalize("relax.size")
def _size(_bb: BlockBuilder, call: Call) -> Expr:
return op.prod(op.shape_to_tensor(op.shape_of(call.args[0])))
@@ -0,0 +1,158 @@
# 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 linear algebra operators."""
from tvm import DataTypeCode, relax, te, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr, Tuple, TupleGetItem, Var
from .common import register_legalize
@register_legalize("relax.matmul")
def _matmul(bb: BlockBuilder, call: Call) -> Expr:
def is_known_tensor_dtype(dtype) -> bool:
raw_dtype = dtype.dtype
return not (
raw_dtype.type_code == int(DataTypeCode.HANDLE)
and raw_dtype.bits == 0
and raw_dtype.lanes == 0
)
def te_matmul(a: te.Tensor, b: te.Tensor) -> te.Tensor:
a_shape = list(a.shape)
b_shape = list(b.shape)
a_prepended = False
b_appended = False
if len(a_shape) == 1:
a_prepended = True
a_shape.insert(0, 1)
if len(b_shape) == 1:
b_appended = True
b_shape.append(1)
is_a_larger = len(a_shape) > len(b_shape)
offset = len(a_shape) - len(b_shape) if is_a_larger else len(b_shape) - len(a_shape)
a_relax = relax.Var("a", relax.TensorType(a.shape))
b_relax = relax.Var("b", relax.TensorType(b.shape))
f_infer_ty = call.op.get_attr("FInferType")
output_shape = f_infer_ty(relax.op.matmul(a_relax, b_relax), bb).shape
if isinstance(a_shape[-1], tirx.IntImm) and a_shape[-1] == 0:
return te.compute(
output_shape,
lambda *_: tirx.const(0, call.ty.dtype),
name="matmul",
)
def matmul_compute(*idx_spatial):
k = te.reduce_axis((0, a_shape[-1]), name="k")
def multiply_compute(idx_reduce):
a_indices = []
b_indices = []
for i in range(offset):
if is_a_larger:
a_indices.append(idx_spatial[i])
else:
b_indices.append(idx_spatial[i])
for i in range(offset, len(output_shape) - (2 - a_prepended - b_appended)):
a_dim = a_shape[i if is_a_larger else i - offset]
b_dim = b_shape[i if not is_a_larger else i - offset]
dim_equal = a_dim == b_dim
if not isinstance(dim_equal, tirx.IntImm) or dim_equal == 0:
a_dim_is_one = isinstance(a_dim, tirx.IntImm) and a_dim == 1
b_dim_is_one = isinstance(b_dim, tirx.IntImm) and b_dim == 1
a_indices.append(0 if a_dim_is_one else idx_spatial[i])
b_indices.append(0 if b_dim_is_one else idx_spatial[i])
else:
a_indices.append(idx_spatial[i])
b_indices.append(idx_spatial[i])
if not a_prepended:
a_indices.append(idx_spatial[-2 + b_appended])
a_indices.append(idx_reduce)
b_indices.append(idx_reduce)
if not b_appended:
b_indices.append(idx_spatial[-1])
dtype = call.attrs.out_dtype
if dtype is not None and dtype != "":
return a(*a_indices).astype(dtype) * b(*b_indices).astype(dtype)
return a(*a_indices) * b(*b_indices)
return te.sum(multiply_compute(k), axis=k)
return te.compute(
output_shape,
lambda *idx: matmul_compute(*idx), # pylint: disable=unnecessary-lambda
name="matmul",
)
lhs, rhs = call.args
lhs_ty = call.args[0].ty
rhs_ty = call.args[1].ty
assert (
lhs_ty.dtype
and rhs_ty.dtype
and is_known_tensor_dtype(lhs_ty.dtype)
and is_known_tensor_dtype(rhs_ty.dtype)
), (
f"To legalize R.matmul into R.call_tir, the dtype of both operands must be known. "
f"However, the LHS {lhs} has type {lhs_ty} (dtype='{lhs_ty.dtype}') "
f"and the RHS {rhs} has type {rhs_ty} (dtype='{rhs_ty.dtype}')."
)
return bb.call_te(te_matmul, call.args[0], call.args[1], primfunc_name_hint="matmul")
@register_legalize("relax.einsum")
def _einsum(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.einsum, call.attrs.subscripts, *fields)
@register_legalize("relax.outer")
def _outer(bb: BlockBuilder, call: Call) -> Expr:
def te_outer(a: te.Tensor, b: te.Tensor) -> te.Tensor:
a_shape = list(a.shape)
b_shape = list(b.shape)
assert len(a_shape) == 1 and len(b_shape) == 1, "outer requires 1D tensors"
n = a_shape[0]
m = b_shape[0]
def compute_fn(i, j):
return a[i] * b[j]
return te.compute((n, m), compute_fn, name="outer")
lhs, rhs = call.args
return bb.call_te(te_outer, lhs, rhs, primfunc_name_hint="outer")
@@ -0,0 +1,369 @@
# 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)
@@ -0,0 +1,826 @@
# 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,unused-argument
"""Default legalization function for neural network operators."""
import logging
import math
from tvm import s_tir, te, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import _call_topi_without_attr, register_legalize
@register_legalize("relax.nn.conv1d")
def _nn_conv1d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.data_layout:
logging.info(
"TOPI conv1d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
if len(call.attrs.data_layout) != 3 or len(call.attrs.kernel_layout) != 3:
logging.info(
"Conv1D where data layout or kernel layout have channel chunk "
"cannot be legalized by TOPI at this moment."
)
return call
if call.attrs.groups != 1:
data_layout = s_tir.slayout(call.attrs.data_layout)
kernel_layout = s_tir.slayout(call.attrs.kernel_layout)
ic = call.args[0].ty.shape.values[data_layout.index_of("C")]
oc = call.args[1].ty.shape.values[kernel_layout.index_of("O")]
if not isinstance(ic, tirx.IntImm) or not isinstance(oc, tirx.IntImm):
logging.info(
"Conv1D where number of groups is more than one and input or output "
"channel size is symbolic cannot be legalized by TOPI at this moment."
)
return call
return bb.call_te(
topi.nn.conv1d,
data=call.args[0],
kernel=call.args[1],
strides=call.attrs.strides,
padding=call.attrs.padding,
dilation=call.attrs.dilation,
groups=call.attrs.groups,
data_layout=call.attrs.data_layout,
kernel_layout=call.attrs.kernel_layout,
out_dtype=call.attrs.out_dtype if call.attrs.out_dtype != "" else None,
primfunc_name_hint="conv1d",
)
@register_legalize("relax.nn.conv2d")
def _nn_conv2d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.data_layout:
logging.info(
"TOPI conv2d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
if len(call.attrs.data_layout) != 4 or len(call.attrs.kernel_layout) != 4:
logging.info(
"Conv2D where data layout or kernel layout have channel chunk "
"cannot be legalized by TOPI at this moment."
)
return call
if call.attrs.groups != 1:
data_layout = s_tir.slayout(call.attrs.data_layout)
kernel_layout = s_tir.slayout(call.attrs.kernel_layout)
ic = call.args[0].ty.shape.values[data_layout.index_of("C")]
oc = call.args[1].ty.shape.values[kernel_layout.index_of("O")]
if not isinstance(ic, tirx.IntImm) or not isinstance(oc, tirx.IntImm):
logging.info(
"Conv2D where number of groups is more than one and input or output "
"channel size is symbolic cannot be legalized by TOPI at this moment."
)
return call
return bb.call_te(
topi.nn.conv,
inp=call.args[0],
filt=call.args[1],
stride=call.attrs.strides,
padding=call.attrs.padding,
dilation=call.attrs.dilation,
groups=call.attrs.groups,
data_layout=call.attrs.data_layout,
kernel_layout=call.attrs.kernel_layout,
out_dtype=call.attrs.out_dtype if call.attrs.out_dtype != "" else None,
primfunc_name_hint="conv2d",
)
@register_legalize("relax.nn.conv3d")
def _nn_conv3d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.data_layout:
logging.info(
"TOPI conv3d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
if len(call.attrs.data_layout) != 5 or len(call.attrs.kernel_layout) != 5:
logging.info(
"Conv3D where data layout or kernel layout have channel chunk "
"cannot be legalized by TOPI at this moment."
)
return call
if call.attrs.groups != 1:
data_layout = s_tir.slayout(call.attrs.data_layout)
kernel_layout = s_tir.slayout(call.attrs.kernel_layout)
ic = call.args[0].ty.shape.values[data_layout.index_of("C")]
oc = call.args[1].ty.shape.values[kernel_layout.index_of("O")]
if not isinstance(ic, tirx.IntImm) or not isinstance(oc, tirx.IntImm):
logging.info(
"Conv3D where number of groups is more than one and input or output "
"channel size is symbolic cannot be legalized by TOPI at this moment."
)
return call
return bb.call_te(
topi.nn.conv,
inp=call.args[0],
filt=call.args[1],
stride=call.attrs.strides,
padding=call.attrs.padding,
dilation=call.attrs.dilation,
groups=call.attrs.groups,
data_layout=call.attrs.data_layout,
kernel_layout=call.attrs.kernel_layout,
out_dtype=call.attrs.out_dtype if call.attrs.out_dtype != "" else None,
primfunc_name_hint="conv3d",
)
@register_legalize("relax.nn.conv1d_transpose")
def _nn_conv1d_transpose(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.data_layout:
logging.info(
"TOPI conv1d_transpose does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
if call.attrs.data_layout != "NCW" or call.attrs.kernel_layout != "IOW":
logging.info(
"TOPI conv1d_transpose does not support input layout other than NCW, "
"and kernel layout other than IOW, so cannot be legalized by TOPI"
)
return call
strides = [int(s) for s in call.attrs.strides]
padding = [int(p) for p in call.attrs.padding]
output_padding = [int(o) for o in call.attrs.output_padding]
groups = int(call.attrs.groups)
out_dtype = call.ty.dtype
dilation = [int(d) for d in call.attrs.dilation]
def te_conv1d_transpose(data, kernel):
# Dilated transposed conv == transposed conv with a spatially dilated (zero-filled) kernel.
if any(d != 1 for d in dilation):
kernel = topi.nn.dilate(kernel, [1, 1, dilation[0]], name="kernel_dilate")
return topi.nn.group_conv1d_transpose_ncw(
data, kernel, strides, padding, out_dtype, output_padding, groups
)
return bb.call_te(
te_conv1d_transpose, call.args[0], call.args[1], primfunc_name_hint="conv1d_transpose"
)
@register_legalize("relax.nn.conv2d_transpose")
def _nn_conv2d_transpose(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.data_layout:
logging.info(
"TOPI conv2d_transpose does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
if call.attrs.data_layout != "NCHW" or call.attrs.kernel_layout != "IOHW":
logging.info(
"TOPI conv2d_transpose does not support input layout other than NCHW, "
"and kernel layout other than IOHW, so cannot be legalized by TOPI"
)
return call
strides = [int(s) for s in call.attrs.strides]
padding = [int(p) for p in call.attrs.padding]
output_padding = [int(o) for o in call.attrs.output_padding]
groups = int(call.attrs.groups)
out_dtype = call.ty.dtype
dilation = [int(d) for d in call.attrs.dilation]
def te_conv2d_transpose(data, kernel):
# Dilated transposed conv == transposed conv with a spatially dilated (zero-filled) kernel.
if any(d != 1 for d in dilation):
kernel = topi.nn.dilate(kernel, [1, 1, dilation[0], dilation[1]], name="kernel_dilate")
return topi.nn.group_conv2d_transpose_nchw(
data, kernel, strides, padding, out_dtype, output_padding, groups
)
return bb.call_te(
te_conv2d_transpose, call.args[0], call.args[1], primfunc_name_hint="conv2d_transpose"
)
@register_legalize("relax.nn.conv3d_transpose")
def _nn_conv3d_transpose(bb: BlockBuilder, call: Call) -> Expr:
# Keep policy in sync with _nn_conv2d_transpose: only lower when TOPI supports
# the layout/dilation.
if call.attrs.out_layout != call.attrs.data_layout:
logging.info(
"TOPI conv3d_transpose does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
if call.attrs.data_layout != "NCDHW" or call.attrs.kernel_layout != "IODHW":
logging.info(
"TOPI conv3d_transpose does not support input layout other than NCDHW, "
"and kernel layout other than IODHW, so cannot be legalized by TOPI"
)
return call
strides = [int(s) for s in call.attrs.strides]
padding = [int(p) for p in call.attrs.padding]
output_padding = [int(o) for o in call.attrs.output_padding]
groups = int(call.attrs.groups)
out_dtype = call.ty.dtype
dilation = [int(d) for d in call.attrs.dilation]
def te_conv3d_transpose(data, kernel):
# Dilated transposed conv == transposed conv with a spatially dilated (zero-filled) kernel.
if any(d != 1 for d in dilation):
kernel = topi.nn.dilate(
kernel, [1, 1, dilation[0], dilation[1], dilation[2]], name="kernel_dilate"
)
return topi.nn.group_conv3d_transpose_ncdhw(
data, kernel, strides, padding, out_dtype, output_padding, groups
)
return bb.call_te(
te_conv3d_transpose, call.args[0], call.args[1], primfunc_name_hint="conv3d_transpose"
)
@register_legalize("relax.nn.pad")
def _nn_pad(bb: BlockBuilder, call: Call) -> Expr:
pad_mode = call.attrs.pad_mode
pad_widths = call.attrs.pad_width
pad_before = pad_widths[::2]
pad_after = pad_widths[1::2]
if pad_mode == "reflect":
return bb.call_te(
topi.nn.reflect_pad, call.args[0], pad_before=pad_before, pad_after=pad_after
)
elif pad_mode == "replicate":
return bb.call_te(
topi.nn.replicate_pad, call.args[0], pad_before=pad_before, pad_after=pad_after
)
elif pad_mode == "circular":
return bb.call_te(
topi.nn.circular_pad, call.args[0], pad_before=pad_before, pad_after=pad_after
)
else:
return bb.call_te(
topi.nn.pad,
call.args[0],
pad_before=pad_before,
pad_after=pad_after,
pad_value=call.attrs.pad_value,
primfunc_name_hint="pad",
)
@register_legalize("relax.nn.pixel_shuffle")
def _nn_pixel_shuffle(bb: BlockBuilder, call: Call) -> Expr:
upscale_factor = call.attrs.upscale_factor
return bb.call_te(topi.nn.pixel_shuffle, call.args[0], upscale_factor=upscale_factor)
@register_legalize("relax.nn.max_pool1d")
def _nn_max_pool1d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI max_pool1d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
return bb.call_te(
topi.nn.pool1d,
call.args[0],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
dilation=call.attrs.dilation,
padding=call.attrs.padding,
pool_type="max",
ceil_mode=call.attrs.ceil_mode,
layout=call.attrs.layout,
primfunc_name_hint="max_pool1d",
)
@register_legalize("relax.nn.max_pool2d")
def _nn_max_pool2d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI max_pool2d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
return bb.call_te(
topi.nn.pool2d,
call.args[0],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
dilation=call.attrs.dilation,
padding=call.attrs.padding,
pool_type="max",
ceil_mode=call.attrs.ceil_mode,
layout=call.attrs.layout,
primfunc_name_hint="max_pool2d",
)
@register_legalize("relax.nn.max_pool3d")
def _nn_max_pool3d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI max_pool3d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
return bb.call_te(
topi.nn.pool3d,
call.args[0],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
dilation=call.attrs.dilation,
padding=call.attrs.padding,
pool_type="max",
ceil_mode=call.attrs.ceil_mode,
layout=call.attrs.layout,
primfunc_name_hint="max_pool3d",
)
@register_legalize("relax.nn.avg_pool1d")
def _nn_avg_pool1d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI avg_pool1d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
return bb.call_te(
topi.nn.pool1d,
call.args[0],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
dilation=call.attrs.dilation,
padding=call.attrs.padding,
pool_type="avg",
ceil_mode=call.attrs.ceil_mode,
layout=call.attrs.layout,
count_include_pad=call.attrs.count_include_pad,
primfunc_name_hint="avg_pool1d",
)
@register_legalize("relax.nn.avg_pool2d")
def _nn_avg_pool2d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI avg_pool2d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
return bb.call_te(
topi.nn.pool2d,
call.args[0],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
dilation=call.attrs.dilation,
padding=call.attrs.padding,
pool_type="avg",
ceil_mode=call.attrs.ceil_mode,
layout=call.attrs.layout,
count_include_pad=call.attrs.count_include_pad,
primfunc_name_hint="avg_pool2d",
)
@register_legalize("relax.nn.avg_pool3d")
def _nn_avg_pool3d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI avg_pool3d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
return bb.call_te(
topi.nn.pool3d,
call.args[0],
kernel=call.attrs.pool_size,
stride=call.attrs.strides,
dilation=call.attrs.dilation,
padding=call.attrs.padding,
pool_type="avg",
ceil_mode=call.attrs.ceil_mode,
layout=call.attrs.layout,
count_include_pad=call.attrs.count_include_pad,
primfunc_name_hint="avg_pool3d",
)
@register_legalize("relax.nn.adaptive_avg_pool1d")
def _nn_adaptive_avg_pool1d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI adaptive_avg_pool1d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
def te_adaptive_avg_pool1d(data, output_size, layout_str):
if output_size is None:
layout = s_tir.slayout(layout_str)
idx_W = layout.index_of("W")
assert idx_W != -1
output_size = data.shape[idx_W]
return topi.nn.adaptive_pool1d(data, output_size, "avg", layout_str)
return bb.call_te(
te_adaptive_avg_pool1d,
call.args[0],
call.attrs.output_size,
call.attrs.layout,
primfunc_name_hint="adaptive_avg_pool1d",
)
@register_legalize("relax.nn.adaptive_avg_pool2d")
def _nn_adaptive_avg_pool2d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI adaptive_avg_pool2d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
def te_adaptive_avg_pool2d(data, output_size, layout_str):
if output_size is None:
layout = s_tir.slayout(layout_str)
idx_H = layout.index_of("H")
idx_W = layout.index_of("W")
assert idx_H != -1 and idx_W != -1
output_size = (data.shape[idx_H], data.shape[idx_W])
return topi.nn.adaptive_pool(data, output_size, "avg", layout_str)
return bb.call_te(
te_adaptive_avg_pool2d,
call.args[0],
call.attrs.output_size,
call.attrs.layout,
primfunc_name_hint="adaptive_avg_pool2d",
)
@register_legalize("relax.nn.adaptive_avg_pool3d")
def _nn_adaptive_avg_pool3d(bb: BlockBuilder, call: Call) -> Expr:
if call.attrs.out_layout != call.attrs.layout:
logging.info(
"TOPI adaptive_avg_pool3d does not support different input-output "
"layouts, and thus cannot be legalized by TOPI"
)
return call
def te_adaptive_avg_pool3d(data, output_size, layout_str):
if output_size is None:
layout = s_tir.slayout(layout_str)
idx_D = layout.index_of("D")
idx_H = layout.index_of("H")
idx_W = layout.index_of("W")
assert idx_D != -1 and idx_H != -1 and idx_W != -1
output_size = (data.shape[idx_D], data.shape[idx_H], data.shape[idx_W])
return topi.nn.adaptive_pool3d(data, output_size, "avg", layout_str)
return bb.call_te(
te_adaptive_avg_pool3d,
call.args[0],
call.attrs.output_size,
call.attrs.layout,
primfunc_name_hint="adaptive_avg_pool3d",
)
register_legalize("relax.nn.relu", _call_topi_without_attr(topi.nn.relu))
@register_legalize("relax.nn.leakyrelu")
def _nn_leakyrelu(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.nn.leaky_relu, call.args[0], call.attrs.alpha)
@register_legalize("relax.nn.prelu")
def _nn_prelu(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.nn.prelu, call.args[0], call.args[1], call.attrs.axis)
@register_legalize("relax.nn.gelu")
def _nn_gelu(bb: BlockBuilder, call: Call) -> Expr:
def te_gelu(x: te.Tensor):
dtype = x.dtype
erf_inp = x * tirx.const(0.5**0.5, dtype)
if dtype == "float16":
erf = topi.math.cast(topi.erf(topi.math.cast(erf_inp, "float32")), "float16")
else:
erf = topi.erf(erf_inp)
return x * (tirx.const(0.5, dtype) + erf * tirx.const(0.5, dtype))
return bb.call_te(te_gelu, call.args[0], primfunc_name_hint="gelu")
@register_legalize("relax.nn.gelu_tanh")
def _nn_gelu_tanh(bb: BlockBuilder, call: Call) -> Expr:
def te_gelu_tanh(x: te.Tensor):
dtype = x.dtype
return (
tirx.const(0.5, dtype)
* x
* (
tirx.const(1.0, dtype)
+ topi.tanh(
tirx.const(math.sqrt(2.0 / math.pi), dtype)
* x
* (1 + tirx.const(0.044715, dtype) * x * x)
)
)
)
return bb.call_te(te_gelu_tanh, call.args[0], primfunc_name_hint="gelu_tanh")
@register_legalize("relax.nn.selu")
def _nn_selu(bb: BlockBuilder, call: Call) -> Expr:
def te_selu(x: te.Tensor):
dtype = x.dtype
alpha = tirx.const(1.6732632423543772848170429916717, dtype)
scale = tirx.const(1.0507009873554804934193349852946, dtype)
# Compute SELU
# SELU(x) = scale*(max(0,x)+min(0,a*(exp(x)-1)))
positive_part = topi.maximum(x, tirx.const(0, dtype))
negative_part = topi.minimum(
tirx.const(0, dtype), alpha * (topi.exp(x) - tirx.const(1, dtype))
)
return scale * (positive_part + negative_part)
return bb.call_te(te_selu, call.args[0], primfunc_name_hint="selu")
@register_legalize("relax.nn.silu")
def _nn_silu(bb: BlockBuilder, call: Call) -> Expr:
def te_silu(x: te.Tensor):
return topi.multiply(x, topi.sigmoid(x))
return bb.call_te(te_silu, call.args[0], primfunc_name_hint="silu")
@register_legalize("relax.nn.softplus")
def _nn_softplus(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.nn.softplus,
call.args[0],
call.attrs.beta,
call.attrs.threshold,
)
@register_legalize("relax.nn.softmax")
def _nn_softmax(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(topi.nn.softmax, call.args[0], call.attrs.axis)
@register_legalize("relax.nn.log_softmax")
def _nn_log_softmax(bb: BlockBuilder, call: Call):
return bb.call_te(topi.nn.log_softmax, call.args[0], call.attrs.axis)
@register_legalize("relax.nn.cross_entropy_with_logits")
def _nn_cross_entropy_with_logits(bb: BlockBuilder, call: Call):
def te_cross_entropy_with_logits(x, y):
if len(x.shape) > 1:
return -topi.sum(x * y) / x.shape[0]
return -topi.sum(x * y)
return bb.call_te(
te_cross_entropy_with_logits,
call.args[0],
call.args[1],
primfunc_name_hint="cross_entropy_with_logits",
)
@register_legalize("relax.nn.batch_norm")
def _nn_batch_norm(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.nn.batch_norm,
data=call.args[0],
gamma=call.args[1],
beta=call.args[2],
moving_mean=call.args[3],
moving_var=call.args[4],
axis=call.attrs.axis,
epsilon=call.attrs.epsilon,
center=call.attrs.center,
scale=call.attrs.scale,
training=call.attrs.training,
momentum=call.attrs.momentum,
)
@register_legalize("relax.nn.layer_norm")
def _nn_layer_norm(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.nn.layer_norm,
call.args[0],
call.args[1],
call.args[2],
axis=call.attrs.axes,
epsilon=call.attrs.epsilon,
)
@register_legalize("relax.nn.group_norm")
def _nn_group_norm(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.nn.group_norm,
call.args[0],
call.args[1],
call.args[2],
call.attrs.num_groups,
call.attrs.channel_axis,
call.attrs.axes,
call.attrs.epsilon,
)
@register_legalize("relax.nn.instance_norm")
def _nn_instance_norm(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.nn.instance_norm,
data=call.args[0],
gamma=call.args[1],
beta=call.args[2],
channel_axis=call.attrs.channel_axis,
axis=call.attrs.axes,
epsilon=call.attrs.epsilon,
)
@register_legalize("relax.nn.rms_norm")
def _nn_rms_norm(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.nn.rms_norm,
call.args[0],
call.args[1],
axis=call.attrs.axes,
epsilon=call.attrs.epsilon,
)
@register_legalize("relax.nn.dropout")
def _nn_dropout(bb: BlockBuilder, call: Call) -> Expr:
# Dropout is a no-op at inference: pass the input through and return an all-ones mask.
return bb.call_te(
lambda x: [topi.identity(x), topi.full_like(x, 1.0)],
call.args[0],
primfunc_name_hint="dropout",
)
def _te_attention(
q: te.Tensor,
k: te.Tensor,
v: te.Tensor,
bias: te.Tensor,
scale: tirx.FloatImm,
causal_mask: str | None,
) -> te.Tensor:
batch_size, seq_len, num_head, head_dim = q.shape
_, seq_len_kv, _, head_dim_v = v.shape
q = topi.transpose(q, [0, 2, 1, 3])
k = topi.transpose(k, [0, 2, 1, 3])
v = topi.transpose(v, [0, 2, 1, 3])
bs = batch_size * num_head
q = topi.reshape(q, [bs, seq_len, head_dim])
k = topi.reshape(k, [bs, seq_len_kv, head_dim])
v = topi.reshape(v, [bs, seq_len_kv, head_dim_v])
p = topi.nn.batch_matmul(q, k, oshape=[bs, seq_len, seq_len_kv])
if scale is not None:
p = topi.multiply(p, scale)
else:
p = topi.divide(p, tirx.sqrt(tirx.Cast(p.dtype, head_dim)))
if bias is not None:
p = topi.reshape(p, [batch_size, num_head, seq_len, seq_len_kv])
p = topi.add(p, bias)
p = topi.reshape(p, [bs, seq_len, seq_len_kv])
if causal_mask is None:
s = topi.nn.softmax(p)
else:
if causal_mask == "TopLeft":
offset = tirx.IntImm("int32", 0)
elif causal_mask == "BottomRight":
offset = tirx.abs(seq_len - seq_len_kv).astype("int32")
else:
raise NotImplementedError()
p_masked = topi.trilu(p, k=offset, upper=False)
p_masked_exp = topi.trilu(
topi.exp(p_masked - topi.max(p_masked, axis=-1, keepdims=True)), k=offset, upper=False
)
p_masked_sum = topi.sum(p_masked_exp, axis=-1, keepdims=True)
s = topi.divide(p_masked_exp, p_masked_sum)
o = topi.nn.batch_matmul(s, v, transpose_b=False, oshape=[bs, seq_len, head_dim_v])
o = topi.reshape(o, [batch_size, num_head, seq_len, head_dim_v])
return topi.transpose(o, [0, 2, 1, 3])
@register_legalize("relax.nn.attention")
def _nn_attention(bb: BlockBuilder, call: Call) -> Expr:
assert call.attrs.window_size is None, (
"Legalization for sliding-window attention is not supported yet."
)
return bb.call_te(
_te_attention,
call.args[0],
call.args[1],
call.args[2],
None,
call.attrs.scale,
call.attrs.causal_mask,
primfunc_name_hint="attention",
)
@register_legalize("relax.nn.attention_bias")
def _nn_attention_bias(bb: BlockBuilder, call: Call) -> Expr:
assert call.attrs.window_size is None, (
"Legalization for sliding-window attention is not supported yet."
)
return bb.call_te(
_te_attention,
call.args[0],
call.args[1],
call.args[2],
call.args[3],
call.attrs.scale,
call.attrs.causal_mask,
primfunc_name_hint="attention_bias",
)
@register_legalize("relax.nn.attention_var_len")
def _nn_attention_var_len(bb: BlockBuilder, call: Call) -> Expr:
raise RuntimeError("Legalization of attention_var_len op is not supported yet.")
@register_legalize("relax.nn.nll_loss")
def _nn_nll_loss(bb: BlockBuilder, call: Call) -> Expr:
def nll_loss_without_weight(predictions, targets, reduction, ignore_index):
weight = topi.full(
(predictions.shape[1] if len(predictions.shape) > 1 else predictions.shape[0],),
predictions.dtype,
1.0,
)
return topi.nn.nll_loss(predictions, targets, weight, reduction, ignore_index)
if len(call.args) == 2:
return bb.call_te(
nll_loss_without_weight,
call.args[0],
call.args[1],
reduction=call.attrs.reduction,
ignore_index=call.attrs.ignore_index,
)
return bb.call_te(
topi.nn.nll_loss,
call.args[0],
call.args[1],
call.args[2],
reduction=call.attrs.reduction,
ignore_index=call.attrs.ignore_index,
)
@register_legalize("relax.nn.batch_flatten")
def _nn_batch_flatten(bb: BlockBuilder, call: Call) -> Expr:
if call.ty.shape is None:
return call
return bb.call_te(topi.reshape, call.args[0], call.ty.shape.values)
@@ -0,0 +1,165 @@
# 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 quantize/dequantize operators."""
import tvm
from tvm import te, tirx
from tvm.ir import Call
from tvm.runtime import DataTypeCode
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import _try_convert_to_scalar_const, register_legalize
def clip_cast(val, dtype):
const_min = tvm.tirx.min_value(dtype)
const_max = tvm.tirx.max_value(dtype)
return te.max(te.min(val, const_max), const_min).astype(dtype)
def is_const_scalar(x):
return isinstance(x, tvm.tirx.IntImm | tvm.tirx.FloatImm)
def _is_singleton_qparam(qparam: te.Tensor) -> bool:
"""Return True if qparam is a tensor with all dimensions equal to 1."""
if not isinstance(qparam, te.Tensor):
return False
if len(qparam.shape) == 0:
return True
for dim in qparam.shape:
if not isinstance(dim, tirx.IntImm) or dim.value != 1:
return False
return True
@register_legalize("relax.quantize")
def _quantize(bb: BlockBuilder, call: Call) -> Expr:
"""
Lower relax.quantize into the sequence of simple operations.
Quantization formula is defined as: out = clip(round(input / scale) + zp, min_val, max_val)
"""
axis = call.attrs.axis
out_dtype = call.attrs.out_dtype
def te_quantize(
data: te.Tensor,
scale: te.Tensor | tirx.IntImm | tirx.FloatImm,
zp: te.Tensor | tirx.IntImm | tirx.FloatImm,
):
scale_singleton = _is_singleton_qparam(scale) if isinstance(scale, te.Tensor) else False
zp_singleton = _is_singleton_qparam(zp) if isinstance(zp, te.Tensor) else False
def quantize_compute(*indices):
if is_const_scalar(scale):
scale_value = scale
elif scale_singleton:
scale_value = scale[(0,) * len(scale.shape)]
else:
scale_value = scale[indices[axis]]
if is_const_scalar(zp):
zp_value = zp
elif zp_singleton:
zp_value = zp[(0,) * len(zp.shape)]
else:
zp_value = zp[indices[axis]]
scaled = data[indices] / scale_value
round_val = (te.round(scaled) if "int" in out_dtype else scaled) + zp_value
return clip_cast(round_val, out_dtype)
output_shape = data.shape
return te.compute(output_shape, quantize_compute, name="quantized")
return bb.call_te(
te_quantize,
call.args[0],
_try_convert_to_scalar_const(call.args[1]),
_try_convert_to_scalar_const(call.args[2]),
primfunc_name_hint="quantize",
)
@register_legalize("relax.dequantize")
def _dequantize(bb: BlockBuilder, call: Call) -> Expr:
"""
Lower relax.dequantize into the sequence of simple operations.
Dequantization formula is defined as: out = scale * (input - zp)
Compute datatype: float32
Example of lowering:
dtype = ["int32"|"float32"]
qnn.dequantize(data, scale, zp, "float32") -->
sub = subtract(cast(data, dtype), zp)
out = multiply(cast(sub, "float32"), scale)
qnn.dequantize(data, scale, zp, "float16") -->
sub = subtract(cast(data, dtype), zp)
mul = multiply(cast(sub, "float32"), cast(scale, "float32"))
clipped_out = clip(mul, float32(-65504.0), float32(65504.0))
out = cast(clipped_out, "float16")
"""
axis = call.attrs.axis
out_dtype = call.attrs.out_dtype
def te_dequantize(
data: te.Tensor,
scale: te.Tensor | tirx.IntImm | tirx.FloatImm,
zp: te.Tensor | tirx.IntImm | tirx.FloatImm,
):
scale_singleton = _is_singleton_qparam(scale) if isinstance(scale, te.Tensor) else False
zp_singleton = _is_singleton_qparam(zp) if isinstance(zp, te.Tensor) else False
def dequantize_compute(*indices):
if is_const_scalar(scale):
scale_value = scale
elif scale_singleton:
scale_value = scale[(0,) * len(scale.shape)]
else:
scale_value = scale[indices[axis]]
if is_const_scalar(zp):
zp_value = zp
elif zp_singleton:
zp_value = zp[(0,) * len(zp.shape)]
else:
zp_value = zp[indices[axis]]
dtype = (
"float32"
if data.dtype.matches_code(DataTypeCode.FLOAT, DataTypeCode.BFLOAT)
else "int32"
)
sub = data[indices].astype(dtype) - zp_value
out = sub * scale_value.astype("float32")
if out_dtype == "float32":
return out
return clip_cast(out, out_dtype)
output_shape = data.shape
return te.compute(output_shape, dequantize_compute, name="dequantized")
return bb.call_te(
te_dequantize,
call.args[0],
_try_convert_to_scalar_const(call.args[1]),
_try_convert_to_scalar_const(call.args[2]),
primfunc_name_hint="dequantize",
)
@@ -0,0 +1,52 @@
# 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 search operators."""
from tvm import topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import LegalizeFunc, TEFunc, _call_topi_without_attr, register_legalize
register_legalize("relax.where", _call_topi_without_attr(topi.where))
def _argmax_argmin(te_func: TEFunc) -> LegalizeFunc:
def argmax_argmin_call_te(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
te_func,
call.args[0],
None if call.attrs.axis is None else call.attrs.axis,
call.attrs.keepdims,
)
return argmax_argmin_call_te
register_legalize("relax.argmax", _argmax_argmin(topi.argmax))
register_legalize("relax.argmin", _argmax_argmin(topi.argmin))
@register_legalize("relax.bucketize")
def _bucketize(bb, call):
input_tensor = call.args[0]
boundaries = call.args[1]
right = call.attrs.right
out_dtype = "int32" if call.attrs.out_int32 else "int64"
return bb.call_te(topi.searchsorted, boundaries, input_tensor, right, out_dtype)
@@ -0,0 +1,179 @@
# 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))
@@ -0,0 +1,72 @@
# 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,unused-argument
"""Default legalization function for unary operators."""
from tvm import te, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import _call_topi_without_attr, register_legalize
# To avoid conflict of IRModule function name and libc function name, we add
# "tir_" as the prefix of the generated PrimFunc name.
register_legalize("relax.abs", _call_topi_without_attr(topi.abs, "tir_abs"))
register_legalize("relax.acos", _call_topi_without_attr(topi.acos, "tir_acos"))
register_legalize("relax.acosh", _call_topi_without_attr(topi.acosh, "tir_acosh"))
register_legalize("relax.asin", _call_topi_without_attr(topi.asin, "tir_asin"))
register_legalize("relax.asinh", _call_topi_without_attr(topi.asinh, "tir_asinh"))
register_legalize("relax.atan", _call_topi_without_attr(topi.atan, "tir_atan"))
register_legalize("relax.atanh", _call_topi_without_attr(topi.atanh, "tir_atanh"))
register_legalize("relax.bitwise_not", _call_topi_without_attr(topi.bitwise_not, "tir_bitwise_not"))
register_legalize("relax.ceil", _call_topi_without_attr(topi.ceil, "tir_ceil"))
register_legalize("relax.cos", _call_topi_without_attr(topi.cos, "tir_cos"))
register_legalize("relax.cosh", _call_topi_without_attr(topi.cosh, "tir_cosh"))
register_legalize("relax.exp", _call_topi_without_attr(topi.exp, "tir_exp"))
register_legalize("relax.floor", _call_topi_without_attr(topi.floor, "tir_floor"))
register_legalize("relax.isfinite", _call_topi_without_attr(topi.isfinite, "tir_isfinite"))
register_legalize("relax.isinf", _call_topi_without_attr(topi.isinf, "tir_isinf"))
register_legalize("relax.isnan", _call_topi_without_attr(topi.isnan, "tir_isnan"))
register_legalize("relax.log", _call_topi_without_attr(topi.log, "tir_log"))
register_legalize("relax.logical_not", _call_topi_without_attr(topi.logical_not, "tir_logical_not"))
register_legalize("relax.negative", _call_topi_without_attr(topi.negative, "tir_negative"))
register_legalize("relax.round", _call_topi_without_attr(topi.round, "tir_round"))
register_legalize("relax.rsqrt", _call_topi_without_attr(topi.rsqrt, "tir_rsqrt"))
register_legalize("relax.sigmoid", _call_topi_without_attr(topi.sigmoid, "tir_sigmoid"))
register_legalize("relax.sign", _call_topi_without_attr(topi.sign, "tir_sign"))
register_legalize("relax.sin", _call_topi_without_attr(topi.sin, "tir_sin"))
register_legalize("relax.sinh", _call_topi_without_attr(topi.sinh, "tir_sinh"))
register_legalize("relax.square", _call_topi_without_attr(lambda x: x * x, "tir_square"))
register_legalize("relax.sqrt", _call_topi_without_attr(topi.sqrt, "tir_sqrt"))
register_legalize("relax.tan", _call_topi_without_attr(topi.tan, "tir_tan"))
register_legalize("relax.tanh", _call_topi_without_attr(topi.tanh, "tir_tanh"))
register_legalize("relax.trunc", _call_topi_without_attr(topi.trunc, "tir_trunc"))
register_legalize("relax.clip", _call_topi_without_attr(topi.clip, "tir_clip"))
@register_legalize("relax.erf")
def _erf(bb: BlockBuilder, call: Call) -> Expr:
def te_erf(x: te.Tensor):
dtype = x.dtype
if dtype == "float16":
erf = topi.math.cast(topi.erf(topi.math.cast(x, "float32")), "float16")
else:
erf = topi.erf(x)
return erf
return bb.call_te(te_erf, call.args[0], primfunc_name_hint="erf")
@@ -0,0 +1,194 @@
# 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.
"""Default legalization function for vision network related operators."""
from tvm import relax, te, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr, TupleGetItem
from .common import register_legalize
@register_legalize("relax.vision.all_class_non_max_suppression")
def _all_class_non_max_suppression(block_builder: BlockBuilder, call: Call) -> Expr:
"""Legalize all_class_non_max_suppression with dynamic output trimming.
This implementation uses dynamic_strided_slice to trim the NMS output to only
contain valid detections, improving memory efficiency and ONNX compatibility.
Returns
-------
result : Expr
The legalized NMS result.
- For ONNX output format, returns a tuple of
`(trimmed_indices, num_total_detections)`, where `trimmed_indices`
contains only valid detection indices.
- For TensorFlow output format, returns the TOPI result directly to
preserve the `(selected_indices, selected_scores, num_detections)`
layout expected by the Relax op.
"""
boxes = call.args[0]
scores = call.args[1]
max_output_boxes_per_class = call.args[2]
iou_threshold = call.args[3]
score_threshold = call.args[4]
output_format = call.attrs.output_format
scores_shape = scores.ty.shape
if len(scores_shape) == 3:
_, _, num_boxes = scores_shape
elif len(scores_shape) == 2:
_, num_boxes = scores_shape
else:
raise ValueError(f"Unexpected scores shape: {scores_shape}")
if isinstance(max_output_boxes_per_class, relax.Constant):
max_boxes_val = int(max_output_boxes_per_class.data.numpy())
else:
max_boxes_val = int(num_boxes)
# Get NMS result with fixed shape from TOPI
nms_result = block_builder.call_te(
topi.vision.all_class_non_max_suppression,
boxes,
scores,
max_boxes_val,
iou_threshold,
score_threshold,
output_format,
)
if output_format == "tensorflow":
return nms_result
selected_indices = block_builder.emit(TupleGetItem(nms_result, 0))
num_total_detections = block_builder.emit(TupleGetItem(nms_result, 1))
# Build slicing parameters using TE to avoid high-level Relax ops during legalization
def build_begin():
return te.compute((2,), lambda i: tirx.const(0, "int64"), name="begin")
def build_strides():
return te.compute((2,), lambda i: tirx.const(1, "int64"), name="strides")
def build_end(count_tensor):
# end = [count_tensor[0], 3]
def compute_end(i):
return tirx.if_then_else(
i == 0,
tirx.Cast("int64", count_tensor[0]),
tirx.const(3, "int64"),
)
return te.compute((2,), compute_end, name="end")
begin = block_builder.call_te(build_begin)
strides = block_builder.call_te(build_strides)
end = block_builder.call_te(build_end, num_total_detections)
# Apply dynamic strided slice to trim to valid detections only
trimmed_indices = block_builder.emit(
relax.op.dynamic_strided_slice(selected_indices, begin, end, strides)
)
# Return trimmed indices along with num_total_detections for compatibility
return relax.Tuple([trimmed_indices, num_total_detections])
@register_legalize("relax.vision.roi_align")
def _roi_align(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.vision.roi_align,
call.args[0],
call.args[1],
pooled_size=call.attrs.pooled_size,
spatial_scale=call.attrs.spatial_scale,
mode=call.attrs.mode,
sample_ratio=call.attrs.sample_ratio,
aligned=call.attrs.aligned,
layout=call.attrs.layout,
)
@register_legalize("relax.vision.get_valid_counts")
def _get_valid_counts(block_builder: BlockBuilder, call: Call) -> Expr:
return block_builder.call_te(
topi.vision.get_valid_counts,
call.args[0],
score_threshold=call.attrs.score_threshold,
id_index=call.attrs.id_index,
score_index=call.attrs.score_index,
)
@register_legalize("relax.vision.non_max_suppression")
def _non_max_suppression(block_builder: BlockBuilder, call: Call) -> Expr:
return block_builder.call_te(
topi.vision.non_max_suppression,
call.args[0],
call.args[1],
call.args[2],
max_output_size=call.attrs.max_output_size,
iou_threshold=call.attrs.iou_threshold,
force_suppress=call.attrs.force_suppress,
top_k=call.attrs.top_k,
coord_start=call.attrs.coord_start,
score_index=call.attrs.score_index,
id_index=call.attrs.id_index,
return_indices=call.attrs.return_indices,
invalid_to_bottom=call.attrs.invalid_to_bottom,
soft_nms_sigma=call.attrs.soft_nms_sigma,
score_threshold=call.attrs.score_threshold,
)
@register_legalize("relax.vision.roi_pool")
def _roi_pool(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.vision.roi_pool,
call.args[0],
call.args[1],
pooled_size=call.attrs.pooled_size,
spatial_scale=call.attrs.spatial_scale,
layout=call.attrs.layout,
)
@register_legalize("relax.vision.multibox_transform_loc")
def _multibox_transform_loc(bb: BlockBuilder, call: Call) -> Expr:
variances = tuple(float(x) for x in call.attrs.variances)
def _te(cls_pred, loc_pred, anchor):
return topi.vision.multibox_transform_loc(
cls_pred,
loc_pred,
anchor,
variances,
clip=call.attrs.clip,
threshold=call.attrs.threshold,
keep_background=call.attrs.keep_background,
)
return bb.call_te(
_te,
call.args[0],
call.args[1],
call.args[2],
primfunc_name_hint="multibox_transform_loc",
)
@@ -0,0 +1,83 @@
# 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.
"""Lower the storage/tensor allocation on IPC memory.
The pass is written in Python for experiment, fast development.
"""
import tvm
from tvm import relax
from tvm.ir.module import IRModule
from tvm.relax.expr import Expr
from tvm.relax.expr_functor import PyExprMutator, mutator
@tvm.transform.module_pass(opt_level=0, name="LowerGPUIPCAllocStorage")
class LowerGPUIPCAllocStorage:
"""Lower the storage/tensor allocation on IPC memory."""
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
return _Rewriter(mod).transform()
@mutator
class _Rewriter(PyExprMutator):
def __init__(self, mod: IRModule) -> None:
super().__init__(mod)
self.mod = mod
self.memory_alloc_storage_op = tvm.ir.Op.get("relax.memory.alloc_storage")
self.memory_alloc_tensor_op = tvm.ir.Op.get("relax.memory.alloc_tensor")
self.builtin_alloc_tensor_op = tvm.ir.Op.get("relax.builtin.alloc_tensor")
def transform(self) -> IRModule:
"""Entry point"""
for g_var, func in self.mod.functions_items():
if isinstance(func, relax.Function):
updated_func = self.visit_expr(func)
self.builder_.update_func(g_var, updated_func)
return self.builder_.get()
def visit_call_(self, call: relax.Call) -> Expr: # pylint: disable=arguments-renamed
if call.op == self.memory_alloc_storage_op and call.args[2].value == "ipc_memory":
return self.rewrite_alloc_storage(call)
elif call.op == self.builtin_alloc_tensor_op and call.args[3].value == "ipc_memory":
return self.rewrite_alloc_tensor(call)
else:
return call
def rewrite_alloc_storage(self, call: relax.Call) -> relax.Call:
shape = call.args[0]
dtype = call.args[3]
return relax.Call(
relax.ExternFunc("runtime.disco.cuda_ipc.alloc_storage"),
args=[shape, dtype],
ty_args=[call.ty],
)
def rewrite_alloc_tensor(self, call: relax.Call) -> relax.Call:
shape = call.args[0]
dtype = call.args[1]
ipc_alloc_storage = relax.Call(
relax.ExternFunc("runtime.disco.cuda_ipc.alloc_storage"),
args=[shape, dtype],
ty_args=[relax.AnyType()],
)
return relax.Call(
self.memory_alloc_tensor_op,
args=[ipc_alloc_storage, call.args[2], shape, dtype, relax.prim_value(0)],
ty_args=call.ty_args,
)
@@ -0,0 +1,88 @@
# 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, unused-argument, redefined-argument-from-local
"""Relax Optimize Layout Transform pass."""
import tvm_ffi
from tvm.ir.module import IRModule
from tvm.ir.transform import PassContext
from tvm.relax import Expr
from tvm.relax.dpl import TuplePattern, is_op, rewrite_call, wildcard
from . import function_pass
@function_pass(opt_level=0)
class OptimizeLayoutTransform:
"""
Pass to remove redundant transform layout operators
introduced by AlterOpImpl pass.
"""
def __init__(self):
self.input = wildcard()
pattern_transform_layout = is_op("relax.layout_transform")(self.input)
pattern_1 = is_op("relax.layout_transform")(pattern_transform_layout)
self.gv_ = wildcard()
args = TuplePattern([pattern_transform_layout])
pattern_2 = is_op("relax.call_tir")(self.gv_, args)
self.pattern_2 = is_op("relax.layout_transform")(pattern_2)
self.pattern = pattern_1 | self.pattern_2
def transform_function(self, func: Expr, mod: IRModule, ctx: PassContext) -> IRModule:
"""
Tranformation function to pattern match layout_transform -> layout_transform
pattern
Parameters
----------
func: Expr
The relax function to be optimized
mod: IRModule
The ir module
ctx: PassContext
Relax pass context
"""
self.mod = mod
updated_func = func
# Skip primitive functions
if "Primitive" in func.attrs.keys() and func.attrs["Primitive"] != 0:
return updated_func
def rewriter(expr, matches):
arg1 = matches[self.pattern]
if self.pattern_2 not in matches.keys():
arg2 = matches[self.input]
else:
arg2 = matches[self.gv_]
if "remove_pad" == self.mod[arg2].attrs["operator_name"]:
arg2 = matches[self.input]
if hasattr(arg1.ty, "shape") and hasattr(arg2.ty, "shape"):
if tvm_ffi.structural_equal(arg1.ty.shape, arg2.ty.shape):
return arg2
return expr
updated_func = rewrite_call(self.pattern, rewriter, func)
return updated_func
@@ -0,0 +1,83 @@
# 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, unused-argument, missing-function-docstring, abstract-method
"""Relax Remove Redundant Reshape ops"""
import tvm_ffi
from tvm import IRModule, relax
from tvm.ir.transform import PassContext
from tvm.relax import Expr
from tvm.relax.dpl import is_op, rewrite_call, wildcard
from . import function_pass
@function_pass(opt_level=0)
class RemoveRedundantReshape:
"""
Transformation pass to remove redundant reshape operator
"""
def __init__(self):
self.input1 = wildcard()
shape1 = wildcard()
pattern_redundant_reshape = is_op("relax.reshape")(self.input1, shape1)
self.no_op_reshape = pattern_redundant_reshape
shape2 = wildcard()
self.repeated_reshape = is_op("relax.reshape")(pattern_redundant_reshape, shape2)
self.pattern = self.repeated_reshape | self.no_op_reshape
def transform_function(self, func: Expr, mod: IRModule, ctx: PassContext) -> IRModule:
"""
Tarnsformation function to remove redundant reshape
where tensors before and after reshape are of same dimentions.
Parameters
--------------
func: Expr
The relax function to be optimized
mod: IRModule
The IR module
ctx: PassContext
Relax pass context
"""
updated_func = func
# Skip primitive functions
if "Primitive" in func.attrs.keys() and func.attrs["Primitive"] != 0:
return updated_func
def rewriter(expr, matches):
arg = matches[self.input1]
if self.repeated_reshape in matches:
output_shape = matches[self.repeated_reshape].args[1]
return relax.op.reshape(arg, output_shape)
elif self.no_op_reshape in matches:
output_shape = matches[self.no_op_reshape].args[1]
if arg.ty.shape and tvm_ffi.structural_equal(arg.ty.shape, output_shape):
return arg
return expr
updated_func = rewrite_call(self.pattern, rewriter, func)
return updated_func
File diff suppressed because it is too large Load Diff