# 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. import tvm import tvm.testing from tvm import relax from tvm.ir import assert_structural_equal from tvm.relax.frontend import nn from tvm.script import ir as I from tvm.script import relax as R def test_linear(): class Activation(nn.Module): define_subroutine = True def forward(self, state: relax.Expr) -> relax.Var: return nn.op.silu(state) class Layer(nn.Module): define_subroutine = True def __init__(self, in_features, out_features): self.weights = nn.Parameter((in_features, out_features), dtype="float32") self.activation = Activation() def forward(self, input: relax.Expr) -> relax.Var: state = nn.op.matmul(input, self.weights) return self.activation(state) @I.ir_module class Expected: @R.function def forward( state: R.Tensor(("batch_size", 64), dtype="float32"), _io: R.Any, weights: R.Tensor((64, 32), dtype="float32"), ) -> R.Tuple(R.Tensor(("batch_size", 32), dtype="float32"), R.Tuple(R.Any)): R.func_attr({"num_input": 2}) with R.dataflow(): state = Expected.layer(state, weights) dataflow_output = (state, (_io,)) R.output(dataflow_output) return dataflow_output @R.function def _initialize_effect() -> R.Tuple(R.Any): with R.dataflow(): _io: R.Any = R.null_value() lv: R.Tuple(R.Any) = (_io,) gv: R.Tuple(R.Any) = lv R.output(gv) return gv @R.function(private=True) def layer( state: R.Tensor(("batch_size", 64), dtype="float32"), weights: R.Tensor((64, 32), dtype="float32"), ) -> R.Tensor(("batch_size", 32), dtype="float32"): with R.dataflow(): state = R.matmul(state, weights) state = Expected.activation(state) dataflow_output = state R.output(dataflow_output) return dataflow_output @R.function(private=True) def activation( state: R.Tensor(("batch_size", 32), dtype="float32"), ) -> R.Tensor(("batch_size", 32), dtype="float32"): with R.dataflow(): state = R.nn.silu(state) dataflow_output = state R.output(dataflow_output) return dataflow_output mod = Layer(64, 32) batch_size = tvm.tirx.Var("batch_size", "int64") tvm_mod, _ = mod.export_tvm( spec={"forward": {"input": nn.spec.Tensor((batch_size, 64), "float32")}}, debug=True ) assert_structural_equal(Expected, tvm_mod, True) def test_different_shapes_produce_distinct_subroutines(): """Regression test: same Module class with different input shapes must generate distinct subroutines, not reuse a cached one.""" class Linear(nn.Module): define_subroutine = True def __init__(self, in_features, out_features): self.weights = nn.Parameter((in_features, out_features), dtype="float32") def forward(self, input: relax.Expr) -> relax.Var: return nn.op.matmul(input, self.weights) class Model(nn.Module): def __init__(self): self.linear_a = Linear(32, 16) self.linear_b = Linear(64, 16) def forward(self, x: relax.Expr, y: relax.Expr) -> relax.Var: a = self.linear_a(x) b = self.linear_b(y) return nn.op.add(a, b) mod = Model() batch_size = tvm.tirx.Var("batch_size", "int64") tvm_mod, _ = mod.export_tvm( spec={ "forward": { "x": nn.spec.Tensor((batch_size, 32), "float32"), "y": nn.spec.Tensor((batch_size, 64), "float32"), } }, debug=True, ) # Collect all private functions (subroutines) in the module subroutine_funcs = [ func for gvar, func in tvm_mod.functions.items() if isinstance(func, relax.Function) and gvar.name_hint not in ( "forward", "_initialize_effect", ) ] # There must be two distinct Linear subroutines (one for in_features=32, # one for in_features=64), not a single cached one reused for both. assert len(subroutine_funcs) == 2, ( f"Expected 2 distinct subroutines for different input shapes, got {len(subroutine_funcs)}" ) if __name__ == "__main__": tvm.testing.main()