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

335 lines
12 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Export `nn.Module` to TVM's IRModule."""
import functools
import operator
import threading
import typing
from tvm import tirx
from tvm.ir import IRModule
from .... import relax as rx
from ...block_builder import BlockBuilder
from ...type import AnyType, ShapeType, TupleType
from . import core, extern
from . import spec as _spec
from .modules import IOEffect
def add_extern(mod: extern.ExternModule) -> None:
"""Add an external module to the exporter."""
try:
exporter = Exporter.current()
except Exception as exception:
raise RuntimeError(
"`nn.add_extern` should only be invoked when exporting a module."
) from exception
exporter.add_external_module(mod)
class Exporter:
"""Builder of ModuleSpec, which exports an nn.Module to TVM IRModule."""
_tls = threading.local()
builder: BlockBuilder
io_effect: core.Effect
extern_mods: list[extern.ExternModule]
def __init__(self, debug: bool) -> None:
self.builder = BlockBuilder()
self.io_effect = IOEffect() if debug else None
self.extern_mods = []
@staticmethod
def current() -> "Exporter":
"""Get the current Exporter under the with scope."""
assert hasattr(Exporter._tls, "current")
return Exporter._tls.current
def __enter__(self) -> "Exporter":
assert not hasattr(Exporter._tls, "current")
Exporter._tls.current = self
return self
def __exit__(self, exc_type, exc, traceback) -> None:
assert hasattr(Exporter._tls, "current")
delattr(Exporter._tls, "current")
def add_external_module(self, mod: extern.ExternModule) -> None:
"""Add an external module to the exporter."""
# pylint: disable=protected-access
all_symbols: list[str] = []
for extern_mod in self.extern_mods:
all_symbols.extend(extern_mod._symbols.keys())
duplicated_symbols = list(set(mod._symbols.keys()) & set(all_symbols))
# pylint: enable=protected-access
if duplicated_symbols:
raise ValueError(f"Duplicate symbols: {duplicated_symbols}")
self.extern_mods.append(mod)
def build( # pylint: disable=too-many-locals
self,
spec: _spec.ModuleSpec,
) -> tuple[
IRModule,
list[tuple[str, core.Parameter]],
list[extern.ExternModule],
]:
"""Build the ModuleSpec to TVM IRModule. Returns the IRModule and the parameters."""
# pylint: disable=protected-access
def _params() -> list[tuple[str, core.Parameter]]:
params = []
for name, param in core._attribute_finder(
spec.module, prefix="", condition_yield=lambda x: isinstance(x, core.Parameter)
):
params.append((name, param))
return params
def _effects() -> list[tuple[str, core.Effect]]:
result = []
if self.io_effect is not None:
result.append(("", self.io_effect))
for name, effect in core._attribute_finder(
spec.module, "", condition_yield=lambda x: isinstance(x, core.Effect)
):
result.append((name, effect))
return result
# pylint: enable=protected-access
params = None
effects = _effects()
ext_mods = self.extern_mods
with self:
if effects:
with self.builder.function("_initialize_effect"):
with self.builder.dataflow():
outputs = _emit_effect_init(self.builder, effects)
self.builder.emit_func_output(outputs, params=[])
for method_name, method_spec in zip(spec.method_names, spec.method_specs):
params = _params() # Re-initialize so symbolic shapes not shared across methods
len_args = len(method_spec.arg_specs)
len_effects = {
"packed": 1,
"none": 0,
"plain": len(effects),
}[method_spec.effect_mode]
with self.builder.function(
method_name,
attrs={"num_input": len_args + len_effects}, # type: ignore
):
with self.builder.dataflow():
outputs, inputs = _emit_method(self.builder, method_spec, params, effects)
self.builder.emit_func_output(outputs, inputs)
mod = self.builder.finalize()
rx.analysis.well_formed(mod)
return mod, params, ext_mods
def _emit_effect_init(
builder: BlockBuilder,
effects: list[tuple[str, core.Effect]],
):
outputs = []
for prefix, effect in effects:
inits = effect.emit_init(prefix, builder)
assert isinstance(inits, list)
outputs.extend(inits)
outputs = builder.emit_output(builder.emit(rx.Tuple(outputs)))
return outputs
def _emit_method( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
builder: BlockBuilder,
spec: _spec.MethodSpec,
params: list[tuple[str, core.Parameter]],
effects: list[tuple[str, core.Effect]] | None,
):
# pylint: disable=protected-access
# symbolic shape's name mapping to its tirx.Var for reuse
str2var_params: dict[str, tirx.Var] = {}
def _unwrap_ret(expr: typing.Any) -> typing.Any:
if isinstance(expr, core.Tensor | core.Object):
return expr._expr
if isinstance(expr, tuple):
return rx.Tuple([_unwrap_ret(x) for x in expr])
if isinstance(expr, list):
return rx.Tuple([_unwrap_ret(x) for x in expr])
raise TypeError(f"Unsupported return type: {type(expr)}")
def _convert_input(arg):
if isinstance(arg, tirx.Var):
return rx.Var(arg.name, ty=ShapeType(values=[arg]))
if isinstance(arg, core.Tensor | core.Object):
return arg._expr # pylint: disable=protected-access
if isinstance(arg, _spec.Tuple):
return rx.Var(
arg.name,
ty=TupleType([_convert_input(arg_i).ty for arg_i in arg.elements]),
)
raise TypeError(f"Unsupported input type: {type(arg)}")
def _params(mode: str) -> list[rx.Var]:
inputs: list[rx.Var] = []
def _get_var(shape_var: tirx.Var) -> tirx.Var:
name = shape_var.name
if name in str2var_params:
return str2var_params[name]
var = tirx.Var(name, "int64")
str2var_params[name] = var
return var
for name, param in params:
# Make sure the a symbolic shape is not re-registered (same as _method_spec_to_inputs)
# e.g. we do not see `vocab_size` for `lm_head` and `vocab_size_1` for `embed_tokens`
new_shape = [_get_var(x) if isinstance(x, tirx.Var) else x for x in param.shape]
var = core.Tensor.placeholder(new_shape, param.dtype, name)._expr
inputs.append(var)
param._expr = var
if mode == "none":
return []
if mode == "plain":
return inputs
if mode == "packed":
input_var = rx.Var(
"packed_params",
TupleType(fields=[x.ty for x in inputs]),
)
for i, (name, param) in enumerate(params):
param._expr = builder.emit(rx.TupleGetItem(input_var, i), name_hint=name)
return [input_var]
raise ValueError(f"Invalid param_mode: {mode}")
def _effects(mode: str) -> list[rx.Var]:
unflat_inputs: list[list[rx.Var]] = []
for name, effect in effects:
effect_input = effect.create(name)
effect.set_state(effect_input)
unflat_inputs.append(effect_input)
inputs: list[rx.Var] = functools.reduce(operator.iadd, unflat_inputs, [])
if mode == "none":
return []
if mode == "plain":
return inputs
if mode == "packed":
input_var = rx.Var(
"packed_effects",
TupleType(fields=[x.ty for x in inputs]),
)
i = 0
for effect_input, (_, effect) in zip(unflat_inputs, effects):
updated_effect_input = []
for effect_input_i in effect_input:
updated_effect_input.append(
builder.emit(
rx.TupleGetItem(input_var, i),
name_hint=effect_input_i.name_hint,
)
)
i += 1
effect.set_state(updated_effect_input)
return [input_var]
raise ValueError(f"Invalid effect_mode: {mode}")
# pylint: enable=protected-access
def _detuple(arg, var: rx.Var, builder: BlockBuilder):
if isinstance(arg, _spec.Tuple):
ret = []
for i, elem in enumerate(arg.elements):
field = builder.emit(rx.TupleGetItem(var, i), name_hint=f"{arg.name}_{i}")
ret.append(_detuple(elem, field, builder))
return type(arg.elements)(ret)
if isinstance(arg, core.Tensor):
return core.Tensor(_expr=var)
if isinstance(arg, tirx.Var):
return arg
raise TypeError(f"Unsupported input type: {type(arg)}")
# TODO(@junrushao): Warn if params/effects are used when their mode is "none"
explicit_inputs = _method_spec_to_inputs(spec)
inputs = [_convert_input(x) for x in explicit_inputs]
inputs = inputs + _effects(spec.effect_mode)
inputs = inputs + _params(spec.param_mode)
for arg_idx, (arg, var) in enumerate(zip(explicit_inputs, inputs)):
if isinstance(arg, _spec.Tuple):
explicit_inputs[arg_idx] = _detuple(arg, var, builder)
outputs = spec.method(*explicit_inputs)
effect_outputs = []
for _, effect in effects:
effect_outputs.extend(effect.finalize())
if effect_outputs and spec.effect_mode != "none":
outputs = builder.emit_output(rx.Tuple([_unwrap_ret(outputs), rx.Tuple(effect_outputs)]))
else:
outputs = builder.emit_output(_unwrap_ret(outputs))
return outputs, inputs
def _method_spec_to_inputs(
spec: _spec.MethodSpec,
) -> list[tirx.Var | core.Tensor]:
"""Convert the MethodSpec to a list of inputs to Module's method."""
str2var: dict[str, tirx.Var] = {}
def _get_var(name: str) -> tirx.Var:
if name in str2var:
return str2var[name]
var = tirx.Var(name, "int64")
str2var[name] = var
return var
def _convert_input(arg_name, arg_spec):
if isinstance(arg_spec, _spec.Int):
arg = _get_var(arg_name)
elif isinstance(arg_spec, _spec.Tensor):
arg = core.Tensor.placeholder( # pylint: disable=protected-access
shape=[_get_var(x) if isinstance(x, str) else x for x in arg_spec.shape],
dtype=arg_spec.dtype,
name=arg_name,
)
elif isinstance(arg_spec, _spec.Object):
arg = arg_spec.object_type(_expr=rx.Var(arg_name, AnyType()), _name=arg_name)
elif isinstance(arg_spec, _spec.Tuple):
elements = type(arg_spec.elements)(
[
_convert_input(arg_name=arg_name + f"_{i}", arg_spec=arg_spec.elements[i])
for i in range(len(arg_spec.elements))
]
)
arg = _spec.Tuple(
name=arg_name,
elements=elements,
)
else:
raise TypeError(f"Invalid spec for argument {arg_name}: {arg_spec}")
return arg
args = []
for arg_name, arg_spec in zip(spec.arg_names, spec.arg_specs):
arg = _convert_input(arg_name=arg_name, arg_spec=arg_spec)
args.append(arg)
return args