189 lines
6.6 KiB
Python
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
|