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

189 lines
6.6 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=too-many-lines,invalid-name,protected-access
"""nn.Module mixin for subroutine dispatch"""
import collections
import contextlib
import functools
import inspect
import re
import typing
import tvm_ffi
from tvm import ir, relax
from tvm.relax.frontend import nn
def _camel_to_snake(name):
"""Convert from CamelCase to snake_case"""
# Adapted from https://stackoverflow.com/a/1176023
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name)
name = name.lower()
return name
def _normalize_expr(block_builder, arg, as_relax_expr=False):
"""Ensure that an argument is a relax.Expr with type"""
if isinstance(arg, tuple):
arg = relax.Tuple([_normalize_expr(block_builder, element) for element in arg])
if isinstance(arg, relax.Expr) and arg.ty.is_missing():
arg = block_builder.emit(arg)
if isinstance(arg, nn.Tensor) and as_relax_expr:
arg = arg._expr
return arg
def _get_ty(arg):
if isinstance(arg, relax.Expr):
return arg.ty
elif isinstance(arg, nn.Tensor):
return arg._expr.ty
elif isinstance(arg, tuple | list | tvm_ffi.Array):
return relax.TupleType([_get_ty(field) for field in arg])
else:
raise TypeError(f"Cannot find type for {arg} of type {type(arg)}")
class SubroutineMixin:
"""A mixin that generates a
Contains common logic for `tvm.relax.frontend.nn.Module` and
`tvm.relax.testing.nn.Module`.
"""
define_subroutine: bool = False
def __init_subclass__(cls):
"""Update the cls.forward of subclasses"""
if hasattr(cls, "forward"):
is_wrapped = getattr(cls.forward, "_is_subroutine_mixin", False)
if not is_wrapped:
cls.forward = cls._subroutine_dispatch(cls.forward)
@classmethod
def _subroutine_dispatch(cls, old_forward):
@functools.wraps(old_forward)
def new_forward(self, *args, **kwargs):
if not self.define_subroutine:
return old_forward(self, *args, **kwargs)
block_builder = relax.BlockBuilder.current()
assert block_builder is not None, (
f"Class {type(self)} has cls.define_subroutines = True, "
"but is called outsdie of a block_builder environment. "
"relax.BlockBuilder.current() is required "
"to determine where to generate the subroutine."
)
func_args = self._normalize_subroutine_args(block_builder, *args, **kwargs)
subroutine, is_nn_tensor_output = self._get_subroutine(
block_builder, old_forward, func_args
)
subroutine_args = [
arg._expr if isinstance(arg, nn.Tensor) else arg
for arg in [*func_args.values(), *self.parameters()]
]
out = subroutine(*subroutine_args)
if is_nn_tensor_output:
if out.ty.is_missing():
out = block_builder.emit(out, name_hint=f"{subroutine.name_hint}_output")
out = nn.Tensor(_expr=out)
return out
new_forward._is_subroutine_mixin = True
return new_forward
def _normalize_subroutine_args(
self, block_builder, *args, **kwargs
) -> typing.OrderedDict[str, relax.Expr]:
signature = inspect.signature(self.forward)
bindings = signature.bind(*args, **kwargs)
func_args = collections.OrderedDict(
(name, _normalize_expr(block_builder, arg)) for name, arg in bindings.arguments.items()
)
return func_args
def _get_subroutine(
self,
block_builder,
old_forward: typing.Callable,
func_args: typing.OrderedDict[str, relax.Expr],
) -> (ir.GlobalVar, bool):
cls = type(self)
if not hasattr(cls, "_gvar"):
cls._gvar = {}
model_params = [
param._expr if isinstance(param, nn.Tensor) else param for param in self.parameters()
]
arg_ty = _get_ty([*func_args.values(), *model_params])
is_dataflow = block_builder.current_block_is_dataflow()
lookup_key = (
old_forward,
tvm_ffi.structural_hash(arg_ty, map_free_vars=True),
is_dataflow,
)
for cached_ty, cached_result in cls._gvar.get(lookup_key, []):
if tvm_ffi.structural_equal(cached_ty, arg_ty, map_free_vars=True):
return cached_result
func_name = _camel_to_snake(cls.__name__)
func_params = [relax.Var(name, ty) for name, ty in zip(func_args, arg_ty.fields)]
old_forward_args = [
nn.Tensor(_expr=param) if isinstance(old_arg, nn.Tensor) else param
for param, old_arg in zip(func_params, func_args.values())
]
with block_builder.function(func_name, [*func_params, *model_params], private=True):
with contextlib.ExitStack() as stack:
if is_dataflow:
stack.enter_context(block_builder.dataflow())
out = old_forward(self, *old_forward_args)
is_nn_tensor_output = isinstance(out, nn.Tensor)
if is_nn_tensor_output:
out = out._expr
if is_dataflow:
out = block_builder.emit_output(out)
gvar = block_builder.emit_func_output(out)
# The relax.Var instances in model_params, along with any
# tirx.Var instances in the type, appear in both the
# calling scope and as parameters for the subroutine. To
# maintain SSA, replace all relax and TIR variables in the
# subroutine.
mod = block_builder.get()
mod.update_func(gvar, relax.utils.copy_with_new_vars(mod[gvar]))
result = (gvar, is_nn_tensor_output)
bucket = cls._gvar.setdefault(lookup_key, [])
bucket.append((arg_ty, result))
return result