# ruff: noqa: E402 import pytest pytest.importorskip("tensorflow", reason="tensorflow not available") # 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=import-self, invalid-name, unused-argument, too-many-lines, len-as-condition, broad-except # pylint: disable=import-outside-toplevel, redefined-builtin """TFLite to Relax converter tests""" import os import flatbuffers import numpy as np import pytest import tensorflow as tf import tflite.Model from tensorflow.keras import applications as keras_app import tvm import tvm.relax.frontend.tflite.tflite_frontend as tflite_frontend from tvm import relax from tvm.relax.frontend.tflite import from_tflite from tvm.script.parser import ir as I from tvm.script.parser import relax as R from tvm.script.parser import tirx as T def _get_mod_from_cfunc(cfunc): converter = tf.lite.TFLiteConverter.from_concrete_functions([cfunc]) converter.target_spec.supported_ops = [ tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS, ] tflite_model_buf = converter.convert() if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(tflite_model_buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(tflite_model_buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") return mod def verify(TestClass, expected=None): if isinstance(TestClass, type): cf = TestClass().func.get_concrete_function() else: cf = TestClass mod = _get_mod_from_cfunc(cf) if expected: tvm.ir.assert_structural_equal(mod, expected) # Run E2E test only on nightly if "CI_ENV_NIGHTLY" not in os.environ: return # Inputs tf_inputs = [] tvm_inputs = [] for arg in mod["main"].params: shape = tuple(shape_val.value for shape_val in arg.ty.shape.values) data = np.random.uniform(0, 1, size=shape).astype(arg.ty.dtype) tvm_inputs.append(data) tf_inputs.append(tf.constant(data)) # TF Run tf_output = cf(*tf_inputs) # TVM Run tgt = tvm.target.Target("c") ex = tvm.compile(mod, tgt) vm = relax.VirtualMachine(ex, tvm.cpu()) vm.set_input("main", *tvm_inputs) vm.invoke_stateful("main") tvm_output = vm.get_outputs("main") if isinstance(tf_output, tuple): for tf_out, tvm_out in zip(tf_output, tvm_output): np.testing.assert_allclose(tf_out.numpy(), tvm_out.numpy(), rtol=1e-5, atol=1e-5) else: np.testing.assert_allclose(tf_output.numpy(), tvm_output.numpy(), rtol=1e-5, atol=1e-5) def _verify_random_with_inputs(cfunc, inputs): """E2E verify random ops by shape/dtype and TVM seeded self-consistency.""" if "CI_ENV_NIGHTLY" not in os.environ: return mod = _get_mod_from_cfunc(cfunc) tvm_inputs = [np.asarray(data) for data in inputs] tf_inputs = [tf.constant(data) for data in tvm_inputs] tf_output = cfunc(*tf_inputs) tgt = tvm.target.Target("c") ex = tvm.compile(mod, tgt) vm = relax.VirtualMachine(ex, tvm.cpu()) def run_tvm(): vm.set_input("main", *tvm_inputs) vm.invoke_stateful("main") return vm.get_outputs("main") tvm_output = run_tvm() tvm_output_again = run_tvm() if not isinstance(tf_output, tuple): tf_output = (tf_output,) tvm_output = (tvm_output,) tvm_output_again = (tvm_output_again,) for tf_out, tvm_out, tvm_out_again in zip(tf_output, tvm_output, tvm_output_again): tf_np = tf_out.numpy() tvm_np = tvm_out.numpy() assert tvm_np.shape == tf_np.shape assert tvm_np.dtype == tf_np.dtype np.testing.assert_equal(tvm_np, tvm_out_again.numpy()) def test_add_one_2d(): class AddOne2D(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 2), dtype=tf.float32)]) def func(self, x): return x + 1 @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add(x, R.const(1.0, "float32")) R.output(gv) return gv verify(AddOne2D, Expected) def test_add_n(): class AddN(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 2), dtype=tf.float32), tf.TensorSpec(shape=(2, 2), dtype=tf.float32), tf.TensorSpec(shape=(2, 2), dtype=tf.float32), ] ) def func(self, x, y, z): return tf.add_n([x, y, z]) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), z: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.add(x, y) gv: R.Tensor((2, 2), dtype="float32") = R.add(lv, z) R.output(gv) return gv verify(AddN, Expected) def test_cumsum(): class Cumsum(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(3, 4), dtype=tf.float32), tf.TensorSpec(shape=(5, 6), dtype=tf.int32), ] ) def func(self, x, y): out1 = tf.math.cumsum(x, axis=0) out2 = tf.math.cumsum(y, axis=1, exclusive=True) return out1, out2 @I.ir_module class Expected: @R.function def main( x: R.Tensor((3, 4), dtype="float32"), y: R.Tensor((5, 6), dtype="int32"), ) -> R.Tuple(R.Tensor((3, 4), dtype="float32"), R.Tensor((5, 6), dtype="int32")): R.func_attr({"num_input": 2}) with R.dataflow(): gv1: R.Tensor((3, 4), dtype="float32") = R.cumsum( x, axis=0, dtype="float32", exclusive=False ) gv2: R.Tensor((5, 6), dtype="int32") = R.cumsum( y, axis=1, dtype="int32", exclusive=True ) gv = (gv1, gv2) R.output(gv) return gv verify(Cumsum, Expected) def test_split(): class Split(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): a, b, c = tf.split(x, 3, axis=1) return tf.raw_ops.Pack(values=[a, b, c], axis=1) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 3, 10), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple( R.Tensor((1, 10), dtype="float32"), R.Tensor((1, 10), dtype="float32"), R.Tensor((1, 10), dtype="float32"), ) = R.split(x, indices_or_sections=3, axis=1) lv1: R.Tensor((1, 10), dtype="float32") = lv[0] lv2: R.Tensor((1, 1, 10), dtype="float32") = R.expand_dims(lv1, axis=[1]) lv3: R.Tensor((1, 10), dtype="float32") = lv[1] lv4: R.Tensor((1, 1, 10), dtype="float32") = R.expand_dims(lv3, axis=[1]) lv5: R.Tensor((1, 10), dtype="float32") = lv[2] lv6: R.Tensor((1, 1, 10), dtype="float32") = R.expand_dims(lv5, axis=[1]) gv: R.Tensor((1, 3, 10), dtype="float32") = R.concat((lv2, lv4, lv6), axis=1) R.output(gv) return gv verify(Split, Expected) def test_split_v_dynamic(): """SPLIT_V with runtime split sizes imports shape-aware Relax IR.""" class TfSplitVDynamic(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(10,), dtype=tf.float32), tf.TensorSpec(shape=(3,), dtype=tf.int32), ] ) def func(self, x, size_splits): return tf.split(x, size_splits, axis=0) @I.ir_module class Expected: @R.function def main( x: R.Tensor((10,), dtype="float32"), size_splits: R.Tensor((3,), dtype="int32"), ) -> R.Tuple( R.Tensor(dtype="float32", ndim=1), R.Tensor(dtype="float32", ndim=1), R.Tensor(dtype="float32", ndim=1), ): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((3,), dtype="int64") = R.cumsum( size_splits, axis=0, dtype="int64", exclusive=False ) lv1: R.Tensor((4,), dtype="int64") = R.concat((R.const([0], "int64"), lv), axis=0) lv2: R.Tensor((1,), dtype="int64") = R.strided_slice( lv1, (R.prim_value(0),), (R.prim_value(0),), (R.prim_value(1),), assume_inbound=False, ) lv3: R.Tensor((1,), dtype="int64") = R.scatter_elements( R.const([0], "int64"), R.const([0], "int64"), lv2, axis=0, reduction="update", ) lv4: R.Shape([10]) = R.shape_of(x) lv5: R.Tensor((1,), dtype="int64") = R.shape_to_tensor(lv4) lv6: R.Tensor((1,), dtype="int64") = R.strided_slice( lv1, (R.prim_value(0),), (R.prim_value(1),), (R.prim_value(2),), assume_inbound=False, ) lv7: R.Tensor((1,), dtype="int64") = R.scatter_elements( lv5, R.const([0], "int64"), lv6, axis=0, reduction="update" ) lv8: R.Tensor(dtype="float32", ndim=1) = R.dynamic_strided_slice( x, lv3, lv7, R.const([1], "int64") ) lv9: R.Tensor((1,), dtype="int64") = R.strided_slice( lv1, (R.prim_value(0),), (R.prim_value(1),), (R.prim_value(2),), assume_inbound=False, ) lv10: R.Tensor((1,), dtype="int64") = R.scatter_elements( R.const([0], "int64"), R.const([0], "int64"), lv9, axis=0, reduction="update", ) lv11: R.Tensor((1,), dtype="int64") = R.strided_slice( lv1, (R.prim_value(0),), (R.prim_value(2),), (R.prim_value(3),), assume_inbound=False, ) lv12: R.Tensor((1,), dtype="int64") = R.scatter_elements( lv5, R.const([0], "int64"), lv11, axis=0, reduction="update" ) lv13: R.Tensor(dtype="float32", ndim=1) = R.dynamic_strided_slice( x, lv10, lv12, R.const([1], "int64") ) lv14: R.Tensor((1,), dtype="int64") = R.strided_slice( lv1, (R.prim_value(0),), (R.prim_value(2),), (R.prim_value(3),), assume_inbound=False, ) lv15: R.Tensor((1,), dtype="int64") = R.scatter_elements( R.const([0], "int64"), R.const([0], "int64"), lv14, axis=0, reduction="update", ) lv16: R.Tensor((1,), dtype="int64") = R.strided_slice( lv1, (R.prim_value(0),), (R.prim_value(3),), (R.prim_value(4),), assume_inbound=False, ) lv17: R.Tensor((1,), dtype="int64") = R.scatter_elements( lv5, R.const([0], "int64"), lv16, axis=0, reduction="update" ) lv18: R.Tensor(dtype="float32", ndim=1) = R.dynamic_strided_slice( x, lv15, lv17, R.const([1], "int64") ) gv: R.Tuple( R.Tensor(dtype="float32", ndim=1), R.Tensor(dtype="float32", ndim=1), R.Tensor(dtype="float32", ndim=1), ) = (lv8, lv13, lv18) R.output(gv) return gv verify(TfSplitVDynamic, Expected) def test_split_v_static(): """SPLIT_V with static unequal size_splits lowers to Relax split.""" class SplitVUnequal(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 10, 4), dtype=tf.float32)]) def func(self, x): return tf.split(x, [2, 3, 5], axis=1) @I.ir_module class ExpectedUnequal: @R.function def main(x: R.Tensor((2, 10, 4), dtype="float32")) -> R.Tuple( R.Tensor((2, 2, 4), dtype="float32"), R.Tensor((2, 3, 4), dtype="float32"), R.Tensor((2, 5, 4), dtype="float32"), ): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple( R.Tensor((2, 2, 4), dtype="float32"), R.Tensor((2, 3, 4), dtype="float32"), R.Tensor((2, 5, 4), dtype="float32"), ) = R.split(x, indices_or_sections=[2, 5], axis=1) lv1: R.Tensor((2, 2, 4), dtype="float32") = lv[0] lv2: R.Tensor((2, 3, 4), dtype="float32") = lv[1] lv3: R.Tensor((2, 5, 4), dtype="float32") = lv[2] gv: R.Tuple( R.Tensor((2, 2, 4), dtype="float32"), R.Tensor((2, 3, 4), dtype="float32"), R.Tensor((2, 5, 4), dtype="float32"), ) = lv1, lv2, lv3 R.output(gv) return gv verify(SplitVUnequal, ExpectedUnequal) def test_pack(): class Pack(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3), dtype=tf.float32), tf.TensorSpec(shape=(2, 3), dtype=tf.float32), ] ) def func(self, x, y): return tf.raw_ops.Pack(values=[x, y], axis=0) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32"), ) -> R.Tensor((2, 2, 3), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((1, 2, 3), dtype="float32") = R.expand_dims(x, axis=[0]) lv1: R.Tensor((1, 2, 3), dtype="float32") = R.expand_dims(y, axis=[0]) gv: R.Tensor((2, 2, 3), dtype="float32") = R.concat((lv, lv1), axis=0) R.output(gv) return gv verify(Pack, Expected) def test_cast(): class Cast(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.cast(x, tf.int32) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="int32") = R.astype(x, dtype="int32") R.output(gv) return gv verify(Cast, Expected) def test_bitcast_float32_to_int32(): """BITCAST same-width: float32 -> int32, shape preserved.""" class BitcastF32ToI32(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.bitcast(x, tf.int32) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="int32") = R.memory.view( x, R.shape([1, 30]), R.dtype("int32") ) R.output(gv) return gv verify(BitcastF32ToI32, Expected) def test_bitcast_uint8_to_int8(): """BITCAST same-width 8-bit: uint8 -> int8.""" class BitcastU8ToI8(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4,), dtype=tf.uint8)]) def func(self, x): return tf.bitcast(x, tf.int8) @I.ir_module class Expected: @R.function def main(x: R.Tensor((4,), dtype="uint8")) -> R.Tensor((4,), dtype="int8"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4,), dtype="int8") = R.memory.view(x, R.shape([4]), R.dtype("int8")) R.output(gv) return gv verify(BitcastU8ToI8, Expected) def test_bitcast_int32_to_int16_widens_shape(): """BITCAST width-changing (smaller): int32[3] -> int16[3, 2].""" class BitcastI32ToI16(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(3,), dtype=tf.int32)]) def func(self, x): return tf.bitcast(x, tf.int16) @I.ir_module class Expected: @R.function def main(x: R.Tensor((3,), dtype="int32")) -> R.Tensor((3, 2), dtype="int16"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((3, 2), dtype="int16") = R.memory.view( x, R.shape([3, 2]), R.dtype("int16") ) R.output(gv) return gv verify(BitcastI32ToI16, Expected) def test_bitcast_int16_to_int32_collapses_shape(): """BITCAST width-changing (larger): int16[5, 2] -> int32[5].""" class BitcastI16ToI32(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(5, 2), dtype=tf.int16)]) def func(self, x): return tf.bitcast(x, tf.int32) @I.ir_module class Expected: @R.function def main(x: R.Tensor((5, 2), dtype="int16")) -> R.Tensor((5,), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((5,), dtype="int32") = R.memory.view(x, R.shape([5]), R.dtype("int32")) R.output(gv) return gv verify(BitcastI16ToI32, Expected) def test_bitwise_xor(): """BITWISE_XOR lowers to relax.op.bitwise_xor.""" class BitwiseXor(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3), dtype=tf.int32), tf.TensorSpec(shape=(2, 3), dtype=tf.int32), ] ) def func(self, x, y): return tf.bitwise.bitwise_xor(x, y) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3), dtype="int32"), y: R.Tensor((2, 3), dtype="int32"), ) -> R.Tensor((2, 3), dtype="int32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="int32") = R.bitwise_xor(x, y) R.output(gv) return gv verify(BitwiseXor, Expected) def test_right_shift(): """RIGHT_SHIFT lowers to relax.op.right_shift.""" class RightShift(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3), dtype=tf.int32), tf.TensorSpec(shape=(2, 3), dtype=tf.int32), ] ) def func(self, x, y): return tf.bitwise.right_shift(x, y) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3), dtype="int32"), y: R.Tensor((2, 3), dtype="int32"), ) -> R.Tensor((2, 3), dtype="int32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="int32") = R.right_shift(x, y) R.output(gv) return gv verify(RightShift, Expected) def test_sign(): """SIGN lowers to relax.op.sign.""" class Sign(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.sign(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.sign(x) R.output(gv) return gv verify(Sign, Expected) def test_unique(): """UNIQUE returns values and inverse indices.""" class Unique(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(6,), dtype=tf.int32)]) def func(self, x): return tf.raw_ops.Unique(x=x, out_idx=tf.int64) @I.ir_module class Expected: @R.function def main( x: R.Tensor((6,), dtype="int32"), ) -> R.Tuple(R.Tensor(dtype="int32", ndim=1), R.Tensor(dtype="int64", ndim=1)): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple(R.Tensor(dtype="int32", ndim=1), R.Tensor(dtype="int64", ndim=1)) = ( R.unique( x, R.prim_value(False), R.prim_value(False), R.prim_value(True), R.prim_value(False), ) ) lv1: R.Tensor(dtype="int32", ndim=1) = lv[0] lv2: R.Tensor(dtype="int64", ndim=1) = lv[1] gv: R.Tuple(R.Tensor(dtype="int32", ndim=1), R.Tensor(dtype="int64", ndim=1)) = ( lv1, lv2, ) R.output(gv) return gv mod = _get_mod_from_cfunc(Unique().func.get_concrete_function()) tvm.ir.assert_structural_equal(mod, Expected) def test_expand_dims(): class ExpandDims(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.expand_dims(x, axis=2) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30, 1), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30, 1), dtype="float32") = R.reshape(x, R.shape([1, 30, 1])) R.output(gv) return gv verify(ExpandDims, Expected) def test_transpose(): class Transpose(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): x = tf.expand_dims(x, axis=2) return tf.transpose(x, perm=[0, 2, 1]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 1, 30), dtype="float32") = R.reshape(x, R.shape([1, 1, 30])) R.output(gv) return gv verify(Transpose, Expected) def test_reshape(): class Reshape(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.reshape(x, (1, 2, 15)) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 2, 15), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 2, 15), dtype="float32") = R.reshape(x, R.shape([1, 2, 15])) R.output(gv) return gv verify(Reshape, Expected) @pytest.mark.parametrize( "input_shape, out_type", [ ((2, 3, 4), tf.int32), ((5,), tf.int64), ((1, 1, 1, 1), tf.int32), ((), tf.int32), ((0, 3), tf.int64), ], ) def test_shape(input_shape, out_type): """SHAPE conversion for static-rank non-quantized tensors.""" class Shape(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=tf.float32)]) def func(self, x): return tf.shape(x, out_type=out_type) out_dtype = "int32" if out_type == tf.int32 else "int64" @I.ir_module class Expected: @R.function def main( x: R.Tensor(input_shape, dtype="float32"), ) -> R.Tensor((len(input_shape),), dtype=out_dtype): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((len(input_shape),), dtype=out_dtype) = R.const( list(input_shape), out_dtype ) R.output(gv) return gv verify(Shape, Expected) def test_shape_dynamic_dim(): """SHAPE conversion with a dynamic input dimension.""" class ShapeDynamic(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(None, 3), dtype=tf.float32)]) def func(self, x): return tf.shape(x, out_type=tf.int32) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 3), dtype="float32")) -> R.Tensor((2,), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Shape([1, 3]) = R.shape_of(x) lv1: R.Tensor((2,), dtype="int64") = R.shape_to_tensor(lv) gv: R.Tensor((2,), dtype="int32") = R.astype(lv1, dtype="int32") R.output(gv) return gv verify(ShapeDynamic, Expected) def _build_rank_model(): """Build a minimal TFLite RANK model.""" builder = flatbuffers.Builder(1024) builtin_op = _get_builtin_operator("RANK") op_code = _build_operator_code(builder, builtin_op) options = _build_empty_builtin_options(builder, "RankOptions") tensors = [ _build_tensor(builder, 0, [2, 3, 4]), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), ] op = _build_operator( builder, 0, [0], [1], builtin_options_type=_get_builtin_options_type("RankOptions"), builtin_options=options, ) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0], outputs=[1]) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=[_build_buffer(builder), _build_buffer(builder)], ) def test_rank(): """RANK emits a static rank constant.""" mod = _load_model_from_buffer(_build_rank_model()) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3, 4), dtype="float32")) -> R.Tensor((), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((), dtype="int32") = R.const(3, "int32") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_bucketize(): """BUCKETIZE lowers to relax.op.bucketize.""" class Bucketize(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.raw_ops.Bucketize(input=x, boundaries=[0.0, 1.0, 3.0]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="int32") = R.bucketize( x, R.const([0.0, 1.0, 3.0], "float32"), out_int32=True, right=False ) R.output(gv) return gv verify(Bucketize, Expected) @pytest.mark.parametrize( "start, limit, delta, dtype", [ (0, 8, 2, tf.int32), (1, 9, 2, tf.int64), (0.0, 1.0, 0.2, tf.float32), (8, 0, -2, tf.int32), (0, 0, 1, tf.int32), (0, 7, 2, tf.int32), (0.0, -1.0, -0.25, tf.float32), ], ) def test_range(start, limit, delta, dtype): """RANGE conversion with non-quantized constant scalar bounds.""" class Range(tf.Module): @tf.function(input_signature=[]) def func(self): return tf.range(start, limit, delta, dtype=dtype) np_dtype = np.float32 if dtype == tf.float32 else np.int64 if dtype == tf.int64 else np.int32 expected_range = np.arange(start, limit, delta, dtype=np_dtype) out_dtype = np.dtype(np_dtype).name @I.ir_module class Expected: @R.function def main() -> R.Tensor((len(expected_range),), dtype=out_dtype): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((len(expected_range),), dtype=out_dtype) = R.const( expected_range, out_dtype ) R.output(gv) return gv verify(Range, Expected) @pytest.mark.parametrize( "start, limit, delta, dtype", [ (2, 13, 3, tf.int32), (8, 0, -2, tf.int32), (0.0, 1.0, 0.25, tf.float32), (1.0, -1.0, -0.5, tf.float32), ], ) def test_range_dynamic_scalar_inputs(start, limit, delta, dtype): """RANGE lowers dynamic (runtime) scalar bounds for both int and float dtypes.""" class RangeDynamic(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(), dtype=dtype), tf.TensorSpec(shape=(), dtype=dtype), tf.TensorSpec(shape=(), dtype=dtype), ] ) def func(self, start, limit, delta): return tf.range(start, limit, delta) cf = RangeDynamic().func.get_concrete_function() mod = _get_mod_from_cfunc(cf) np_dtype = np.int32 if dtype == tf.int32 else np.float32 inputs = [ np.array(start, np_dtype), np.array(limit, np_dtype), np.array(delta, np_dtype), ] ex = tvm.compile(mod, tvm.target.Target("llvm")) vm = relax.VirtualMachine(ex, tvm.cpu()) vm.set_input("main", *inputs) vm.invoke_stateful("main") tvm_out = vm.get_outputs("main").numpy() expected = np.arange(start, limit, delta, dtype=np_dtype) np.testing.assert_allclose(tvm_out, expected, rtol=1e-5, atol=1e-5) def test_tile_ir(): """TILE conversion with explicit Relax IR structural check.""" class Tile(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.tile(x, [2, 1]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((4, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4, 3), dtype="float32") = R.tile(x, repeats=[2, 1]) R.output(gv) return gv verify(Tile, Expected) @pytest.mark.parametrize( "input_shape, multiples, dtype", [ ((2, 3), [2, 1], tf.float32), ((1, 4, 2), [3, 1, 2], tf.float32), ((2, 1, 3, 1), [1, 2, 1, 4], tf.float32), ((3,), [2], tf.float32), ((2, 3), [4, 2], tf.float32), ((2, 2), [1, 3], tf.int32), ], ) def test_tile(input_shape, multiples, dtype): """TILE conversion for non-quantized input and repeat factors.""" class Tile(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=dtype)]) def func(self, x): return tf.tile(x, multiples) if input_shape == (2, 3) and multiples == [2, 1]: @I.ir_module class ExpectedTile2x3Repeat2x1: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((4, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4, 3), dtype="float32") = R.tile(x, repeats=[2, 1]) R.output(gv) return gv expected = ExpectedTile2x3Repeat2x1 elif input_shape == (1, 4, 2): @I.ir_module class ExpectedTile1x4x2: @R.function def main(x: R.Tensor((1, 4, 2), dtype="float32")) -> R.Tensor( (3, 4, 4), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((3, 4, 4), dtype="float32") = R.tile(x, repeats=[3, 1, 2]) R.output(gv) return gv expected = ExpectedTile1x4x2 elif input_shape == (2, 1, 3, 1): @I.ir_module class ExpectedTile2x1x3x1: @R.function def main(x: R.Tensor((2, 1, 3, 1), dtype="float32")) -> R.Tensor( (2, 2, 3, 4), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2, 3, 4), dtype="float32") = R.tile(x, repeats=[1, 2, 1, 4]) R.output(gv) return gv expected = ExpectedTile2x1x3x1 elif input_shape == (3,): @I.ir_module class ExpectedTile3: @R.function def main(x: R.Tensor((3,), dtype="float32")) -> R.Tensor((6,), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((6,), dtype="float32") = R.tile(x, repeats=[2]) R.output(gv) return gv expected = ExpectedTile3 elif input_shape == (2, 3) and multiples == [4, 2]: @I.ir_module class ExpectedTile2x3Repeat4x2: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((8, 6), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((8, 6), dtype="float32") = R.tile(x, repeats=[4, 2]) R.output(gv) return gv expected = ExpectedTile2x3Repeat4x2 else: @I.ir_module class ExpectedTileInt32: @R.function def main(x: R.Tensor((2, 2), dtype="int32")) -> R.Tensor((2, 6), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 6), dtype="int32") = R.tile(x, repeats=[1, 3]) R.output(gv) return gv expected = ExpectedTileInt32 verify(Tile, expected) def test_tile_identity(): """TILE with all repeat factors set to one imports as identity.""" class Tile(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.tile(x, [1, 1]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = x R.output(gv) return gv verify(Tile, Expected) def test_concat_v2(): class ConcatV2(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): a, b, c = tf.split(x, 3, axis=1) axis = tf.add(tf.constant(1, dtype="int32"), tf.constant(0, dtype="int32")) return tf.raw_ops.ConcatV2(values=[a, b, c], axis=axis) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple( R.Tensor((1, 10), dtype="float32"), R.Tensor((1, 10), dtype="float32"), R.Tensor((1, 10), dtype="float32"), ) = R.split(x, indices_or_sections=3, axis=1) lv1: R.Tensor((1, 10), dtype="float32") = lv[0] lv2: R.Tensor((1, 10), dtype="float32") = lv[1] lv3: R.Tensor((1, 10), dtype="float32") = lv[2] gv: R.Tensor((1, 30), dtype="float32") = R.concat((lv1, lv2, lv3), axis=1) R.output(gv) return gv verify(ConcatV2, Expected) def test_multi_output(): class MultiOutput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 2), dtype=tf.float32)]) def func(self, x): y = 2 * x return x, y @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.multiply(x, R.const(2.0, "float32")) gv: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = (x, lv) R.output(gv) return gv verify(MultiOutput, Expected) def test_elu(): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.nn.elu(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 30), dtype="float32") = R.exp(x) lv1: R.Tensor((1, 30), dtype="float32") = R.subtract(R.const(1.0, "float32"), lv) lv2: R.Tensor((1, 30), dtype="float32") = R.nn.relu(lv1) lv3: R.Tensor((1, 30), dtype="float32") = R.multiply(R.const(-1.0, "float32"), lv2) lv4: R.Tensor((1, 30), dtype="float32") = R.nn.relu(x) gv: R.Tensor((1, 30), dtype="float32") = R.add(lv3, lv4) R.output(gv) return gv verify(TfInput, Expected) def test_gelu(): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.nn.gelu(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 30), dtype="float32") = R.multiply( x, R.const(0.70710676908493042, "float32") ) lv1: R.Tensor((1, 30), dtype="float32") = R.erf(lv) lv2: R.Tensor((1, 30), dtype="float32") = R.multiply(lv1, R.const(0.5, "float32")) lv3: R.Tensor((1, 30), dtype="float32") = R.add(R.const(0.5, "float32"), lv2) gv: R.Tensor((1, 30), dtype="float32") = R.multiply(x, lv3) R.output(gv) return gv verify(TfInput, Expected) def test_swish(): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.nn.swish(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 30), dtype="float32") = R.sigmoid(x) gv: R.Tensor((1, 30), dtype="float32") = R.multiply(x, lv) R.output(gv) return gv verify(TfInput, Expected) def test_prelu_constant_alpha(): alpha = np.linspace(0.1, 0.3, 30, dtype=np.float32) alpha_init = tf.keras.initializers.Constant(alpha) prelu = tf.keras.layers.PReLU(alpha_initializer=alpha_init) class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return prelu(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 30), dtype="float32") = R.broadcast_to( R.const(alpha), R.shape([1, 30]) ) lv1: R.Tensor((30,), dtype="float32") = R.reshape(x, R.shape([30])) lv2: R.Tensor((30,), dtype="float32") = R.reshape(lv, R.shape([30])) lv3: R.Tensor((30,), dtype="float32") = R.nn.prelu(lv1, lv2, axis=0) gv: R.Tensor((1, 30), dtype="float32") = R.reshape(lv3, R.shape([1, 30])) R.output(gv) return gv verify(TfInput, Expected) def test_fill(): class TfInput(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(1, 30), dtype=tf.float32), tf.TensorSpec(shape=(), dtype=tf.float32), ] ) def func(self, x, y): fill_out = tf.fill((1, 30), y) return x + fill_out @I.ir_module class Expected: @R.function def main( x: R.Tensor((1, 30), dtype="float32"), y: R.Tensor((), dtype="float32") ) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="float32") = R.add(x, y) R.output(gv) return gv verify(TfInput, Expected) def test_fill_dynamic_dims(): """FILL with runtime dims legalizes and compiles.""" class TfFillDynamic(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2,), dtype=tf.int32), tf.TensorSpec(shape=(), dtype=tf.float32), ] ) def func(self, dims, value): return tf.fill(dims, value) @I.ir_module class Expected: @R.function def main( dims: R.Tensor((2,), dtype="int32"), value: R.Tensor((), dtype="float32") ) -> R.Tensor(dtype="float32", ndim=2): R.func_attr({"num_input": 2}) fill_dim_0 = T.int64() fill_dim_1 = T.int64() with R.dataflow(): lv: R.Tensor((2,), dtype="int32") = R.match_cast( dims, R.Tensor((2,), dtype="int32") ) lv1: R.Tensor((2,), dtype="int64") = R.astype(lv, dtype="int64") lv2: R.Shape(ndim=2) = R.tensor_to_shape(lv1) _: R.Shape([fill_dim_0, fill_dim_1]) = R.match_cast( lv2, R.Shape([fill_dim_0, fill_dim_1]) ) gv: R.Tensor((fill_dim_0, fill_dim_1), dtype="float32") = R.full( R.shape([fill_dim_0, fill_dim_1]), value ) R.output(gv) return gv verify(TfFillDynamic, Expected) def test_random_uniform_dynamic_shape(): """RANDOM_UNIFORM imports dynamic shape and validates random output metadata.""" class TfRandomUniform(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2,), dtype=tf.int32)]) def func(self, shape): return tf.raw_ops.RandomUniform(shape=shape, dtype=tf.float32, seed=7, seed2=11) @I.ir_module class Expected: @R.function def main(shape: R.Tensor((2,), dtype="int32")) -> R.Tensor(dtype="float32", ndim=2): R.func_attr({"num_input": 1}) random_uniform_dim_0 = T.int64() random_uniform_dim_1 = T.int64() with R.dataflow(): lv: R.Tensor((2,), dtype="int32") = R.match_cast( shape, R.Tensor((2,), dtype="int32") ) lv1: R.Tensor((2,), dtype="int64") = R.astype(lv, dtype="int64") lv2: R.Shape(ndim=2) = R.tensor_to_shape(lv1) _: R.Shape([random_uniform_dim_0, random_uniform_dim_1]) = R.match_cast( lv2, R.Shape([random_uniform_dim_0, random_uniform_dim_1]) ) gv = R.call_dps_packed( "tvm.contrib.random.uniform", ( R.prim_value(7), R.prim_value(11), R.prim_value(T.float64(0.0)), R.prim_value(T.float64(1.0)), ), out_ty=R.Tensor((random_uniform_dim_0, random_uniform_dim_1), dtype="float32"), ) R.output(gv) return gv cf = TfRandomUniform().func.get_concrete_function() mod = _get_mod_from_cfunc(cf) tvm.ir.assert_structural_equal(mod, Expected) _verify_random_with_inputs(cf, [np.array([2, 3], dtype="int32")]) def test_random_standard_normal_dynamic_shape(): """RANDOM_STANDARD_NORMAL imports dynamic shape and validates random output metadata.""" class TfRandomStandardNormal(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2,), dtype=tf.int32)]) def func(self, shape): return tf.raw_ops.RandomStandardNormal(shape=shape, dtype=tf.float32, seed=3, seed2=5) @I.ir_module class Expected: @R.function def main(shape: R.Tensor((2,), dtype="int32")) -> R.Tensor(dtype="float32", ndim=2): R.func_attr({"num_input": 1}) random_standard_normal_dim_0 = T.int64() random_standard_normal_dim_1 = T.int64() with R.dataflow(): lv: R.Tensor((2,), dtype="int32") = R.match_cast( shape, R.Tensor((2,), dtype="int32") ) lv1: R.Tensor((2,), dtype="int64") = R.astype(lv, dtype="int64") lv2: R.Shape(ndim=2) = R.tensor_to_shape(lv1) _: R.Shape([random_standard_normal_dim_0, random_standard_normal_dim_1]) = ( R.match_cast( lv2, R.Shape([random_standard_normal_dim_0, random_standard_normal_dim_1]) ) ) gv = R.call_dps_packed( "tvm.contrib.random.normal", ( R.prim_value(3), R.prim_value(5), R.prim_value(T.float64(0.0)), R.prim_value(T.float64(1.0)), ), out_ty=R.Tensor( (random_standard_normal_dim_0, random_standard_normal_dim_1), dtype="float32", ), ) R.output(gv) return gv cf = TfRandomStandardNormal().func.get_concrete_function() mod = _get_mod_from_cfunc(cf) tvm.ir.assert_structural_equal(mod, Expected) _verify_random_with_inputs(cf, [np.array([2, 4], dtype="int32")]) def test_multinomial_dynamic_num_samples(): """MULTINOMIAL lowers through seeded uniform sampling with dynamic num_samples.""" class TfMultinomial(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3), dtype=tf.float32), tf.TensorSpec(shape=(), dtype=tf.int32), ] ) def func(self, logits, num_samples): return tf.raw_ops.Multinomial( logits=logits, num_samples=num_samples, output_dtype=tf.int64, seed=13, seed2=17, ) @I.ir_module class Expected: @R.function def main( logits: R.Tensor((2, 3), dtype="float32"), num_samples: R.Tensor((), dtype="int32"), ) -> R.Tensor(dtype="int64", ndim=2): R.func_attr({"num_input": 2}) multinomial_num_samples = T.int64() with R.dataflow(): lv: R.Tensor((), dtype="int32") = R.match_cast( num_samples, R.Tensor((), dtype="int32") ) lv1: R.Tensor((), dtype="int64") = R.astype(lv, dtype="int64") lv2: R.Tensor((1,), dtype="int64") = R.reshape(lv1, R.shape([1])) lv3: R.Shape(ndim=1) = R.tensor_to_shape(lv2) _: R.Shape([multinomial_num_samples]) = R.match_cast( lv3, R.Shape([multinomial_num_samples]) ) lv5: R.Tensor((2, 3), dtype="float32") = R.nn.softmax(logits, axis=-1) lv6 = R.call_dps_packed( "tvm.contrib.random.uniform", ( R.prim_value(13), R.prim_value(17), R.prim_value(T.float64(0.0)), R.prim_value(T.float64(1.0)), ), out_ty=R.Tensor((2 * multinomial_num_samples, 1), dtype="float32"), ) lv7: R.Tensor((2,), dtype="int64") = R.arange( R.prim_value(0), R.prim_value(2), R.prim_value(1), dtype="int64" ) lv8: R.Tensor((2, 1), dtype="int64") = R.expand_dims(lv7, axis=[1]) lv9: R.Tensor((2, multinomial_num_samples), dtype="int64") = R.broadcast_to( lv8, R.shape([2, multinomial_num_samples]) ) lv10: R.Tensor((2 * multinomial_num_samples, 1), dtype="int64") = R.reshape( lv9, R.shape([2 * multinomial_num_samples, 1]) ) lv11: R.Tensor((2 * multinomial_num_samples, 1), dtype="int64") = ( R.multinomial_from_uniform(lv5, lv6, lv10, dtype="int64") ) gv: R.Tensor((2, multinomial_num_samples), dtype="int64") = R.reshape( lv11, R.shape([2, multinomial_num_samples]) ) R.output(gv) return gv cf = TfMultinomial().func.get_concrete_function() mod = _get_mod_from_cfunc(cf) tvm.ir.assert_structural_equal(mod, Expected) _verify_random_with_inputs( cf, [ np.array([[2.0, 1.0, 0.5], [0.1, 0.2, 3.0]], dtype="float32"), np.array(4, dtype="int32"), ], ) @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.add, R.add), (tf.subtract, R.subtract), (tf.multiply, R.multiply), (tf.divide, R.divide), (tf.math.floormod, R.floor_mod), (tf.math.floordiv, R.floor_divide), (tf.math.atan2, R.atan2), ], ) def test_binary(tf_op, relax_op): class Binary(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 2), dtype=tf.float32), tf.TensorSpec(shape=(2, 2), dtype=tf.float32), ] ) def func(self, x, y): return tf_op(x, y) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32") ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = relax_op(x, y) R.output(gv) return gv verify(Binary, Expected) def test_pow(): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.math.pow(x, 4) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="float32") = R.power(x, R.const(4.0, "float32")) R.output(gv) return gv verify(TfInput, Expected) def test_square(): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.math.square(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="float32") = R.power(x, R.const(2.0, "float32")) R.output(gv) return gv verify(TfInput, Expected) def test_broadcast_args(): class TfInput(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(3,), dtype=tf.int32), tf.TensorSpec(shape=(3,), dtype=tf.int32), ] ) def func(self, s0, s1): return tf.broadcast_dynamic_shape(s0, s1) @I.ir_module class Expected: @R.function def main(s0: R.Tensor((3,), dtype="int32"), s1: R.Tensor((3,), dtype="int32")) -> R.Tensor( (3,), dtype="int32" ): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((0,), dtype="int32") = R.full( R.shape([0]), R.const(1, "int32"), dtype="int32" ) lv1: R.Tensor((3,), dtype="int32") = R.concat((lv, s0), axis=0) lv2: R.Tensor((3,), dtype="bool") = R.equal(lv1, R.const(1, "int32")) lv3: R.Tensor((0,), dtype="int32") = R.full( R.shape([0]), R.const(1, "int32"), dtype="int32" ) lv4: R.Tensor((3,), dtype="int32") = R.concat((lv3, s1), axis=0) lv5: R.Tensor((3,), dtype="bool") = R.equal(lv4, R.const(1, "int32")) lv6: R.Tensor((3,), dtype="int32") = R.maximum(lv1, lv4) lv7: R.Tensor((3,), dtype="int32") = R.where(lv5, lv1, lv6) gv: R.Tensor((3,), dtype="int32") = R.where(lv2, lv4, lv7) R.output(gv) return gv verify(TfInput, Expected) def test_broadcast_args_diff_length(): """BROADCAST_ARGS with shape inputs of different lengths.""" class TfInput(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(1,), dtype=tf.int32), tf.TensorSpec(shape=(3,), dtype=tf.int32), ] ) def func(self, s0, s1): return tf.broadcast_dynamic_shape(s0, s1) @I.ir_module class Expected: @R.function def main(s0: R.Tensor((1,), dtype="int32"), s1: R.Tensor((3,), dtype="int32")) -> R.Tensor( (3,), dtype="int32" ): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2,), dtype="int32") = R.full( R.shape([2]), R.const(1, "int32"), dtype="int32" ) lv1: R.Tensor((3,), dtype="int32") = R.concat((lv, s0), axis=0) lv2: R.Tensor((3,), dtype="bool") = R.equal(lv1, R.const(1, "int32")) lv3: R.Tensor((0,), dtype="int32") = R.full( R.shape([0]), R.const(1, "int32"), dtype="int32" ) lv4: R.Tensor((3,), dtype="int32") = R.concat((lv3, s1), axis=0) lv5: R.Tensor((3,), dtype="bool") = R.equal(lv4, R.const(1, "int32")) lv6: R.Tensor((3,), dtype="int32") = R.maximum(lv1, lv4) lv7: R.Tensor((3,), dtype="int32") = R.where(lv5, lv1, lv6) gv: R.Tensor((3,), dtype="int32") = R.where(lv2, lv4, lv7) R.output(gv) return gv verify(TfInput, Expected) @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.nn.relu, R.nn.relu), (tf.nn.relu6, R.nn.relu6), (tf.math.floor, R.floor), (tf.math.ceil, R.ceil), (tf.math.tanh, R.tanh), (tf.math.sigmoid, R.sigmoid), (tf.math.abs, R.abs), (tf.math.cos, R.cos), (tf.math.sin, R.sin), (tf.math.exp, R.exp), (tf.math.log, R.log), (tf.math.negative, R.negative), (tf.round, R.round), (tf.math.rsqrt, R.rsqrt), (tf.nn.softmax, R.nn.softmax), (tf.math.sqrt, R.sqrt), (tf.nn.log_softmax, R.nn.log_softmax), ], ) def test_element_wise(tf_op, relax_op): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf_op(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="float32") = relax_op(x) R.output(gv) return gv verify(TfInput, Expected) @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.math.less, R.less), (tf.math.less_equal, R.less_equal), (tf.math.greater, R.greater), (tf.math.greater_equal, R.greater_equal), (tf.math.equal, R.equal), (tf.math.not_equal, R.not_equal), ], ) def test_split_compare(tf_op, relax_op): class Compare(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): a, b = tf.split(x, 2, axis=1) return tf_op(a, b, name=None) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 15), dtype="bool"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple( R.Tensor((1, 15), dtype="float32"), R.Tensor((1, 15), dtype="float32"), ) = R.split(x, indices_or_sections=2, axis=1) lv1: R.Tensor((1, 15), dtype="float32") = lv[0] lv2: R.Tensor((1, 15), dtype="float32") = lv[1] gv: R.Tensor((1, 15), dtype="bool") = relax_op(lv1, lv2) R.output(gv) return gv verify(Compare, Expected) @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.math.logical_not, R.logical_not), ], ) def test_logical_unary(tf_op, relax_op): class Logical(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 2), dtype=tf.bool), ] ) def func(self, x): return tf_op(x) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="bool"), ) -> R.Tensor((2, 2), dtype="bool"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="bool") = relax_op(x) R.output(gv) return gv verify(Logical, Expected) @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.math.logical_or, R.logical_or), (tf.math.logical_and, R.logical_and), ], ) def test_logical(tf_op, relax_op): class Logical(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 2), dtype=tf.bool), tf.TensorSpec(shape=(2, 2), dtype=tf.bool), ] ) def func(self, x, y): return tf_op(x, y) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="bool"), y: R.Tensor((2, 2), dtype="bool")) -> R.Tensor( (2, 2), dtype="bool" ): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="bool") = relax_op(x, y) R.output(gv) return gv verify(Logical, Expected) @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.add, R.add), (tf.subtract, R.subtract), (tf.multiply, R.multiply), (tf.divide, R.divide), (tf.math.floormod, R.floor_mod), (tf.math.maximum, R.maximum), (tf.math.minimum, R.minimum), ], ) def test_split_binary(tf_op, relax_op): class Binary(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): a, b = tf.split(x, 2, axis=1) return tf_op(a, b, name=None) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 15), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple( R.Tensor((1, 15), dtype="float32"), R.Tensor((1, 15), dtype="float32"), ) = R.split(x, indices_or_sections=2, axis=1) lv1: R.Tensor((1, 15), dtype="float32") = lv[0] lv2: R.Tensor((1, 15), dtype="float32") = lv[1] gv: R.Tensor((1, 15), dtype="float32") = relax_op(lv1, lv2) R.output(gv) return gv verify(Binary, Expected) def test_squared_difference(): class SquaredDifference(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3), dtype=tf.float32), tf.TensorSpec(shape=(2, 3), dtype=tf.float32), ] ) def func(self, x, y): return tf.math.squared_difference(x, y) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32") ) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2, 3), dtype="float32") = R.subtract(x, y) gv: R.Tensor((2, 3), dtype="float32") = R.power(lv, R.const(2.0, "float32")) R.output(gv) return gv verify(SquaredDifference, Expected) @pytest.mark.parametrize( "tf_op, relax_op, axis, out_shape", [ (tf.math.argmax, R.argmax, 0, (30,)), (tf.math.argmin, R.argmin, 1, (5,)), ], ) def test_reduce(tf_op, relax_op, axis, out_shape): class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(5, 30), dtype=tf.float32)]) def func(self, x): return tf_op(x, axis=axis) @I.ir_module class Expected: @R.function def main(x: R.Tensor((5, 30), dtype="float32")) -> R.Tensor(out_shape, dtype="int64"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor(out_shape, dtype="int64") = relax_op(x, axis=axis, keepdims=False) R.output(gv) return gv verify(TfInput, Expected) def test_fully_connected(): class FullyConnected(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 8), dtype=tf.float32)]) def func(self, x): weight = tf.constant(np.arange(24, dtype=np.float32).reshape((3, 8))) bias = tf.constant(np.array([0.5, 1.0, -1.0], dtype=np.float32)) out = tf.matmul(x, weight, transpose_b=True) return tf.nn.bias_add(out, bias) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 8), dtype="float32")) -> R.Tensor((1, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((8, 3), dtype="float32") = R.permute_dims( R.const(np.arange(24, dtype=np.float32).reshape((3, 8))), axes=[1, 0] ) lv1: R.Tensor((1, 3), dtype="float32") = R.matmul(x, lv) gv: R.Tensor((1, 3), dtype="float32") = R.add( lv1, R.const(np.array([0.5, 1.0, -1.0], dtype=np.float32)) ) R.output(gv) return gv verify(FullyConnected, Expected) def test_depthwise_conv2d(): class DepthwiseConv2D(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(1, 8, 8, 2), dtype=tf.float32), tf.TensorSpec(shape=(3, 3, 2, 1), dtype=tf.float32), ] ) def func(self, data, kernel): return tf.nn.depthwise_conv2d( input=data, filter=kernel, strides=[1, 1, 1, 1], padding="SAME", ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 8, 8, 2), dtype="float32"), kernel: R.Tensor((3, 3, 2, 1), dtype="float32"), ) -> R.Tensor((1, 8, 8, 2), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((1, 3, 3, 2), dtype="float32") = R.reshape( kernel, R.shape([1, 3, 3, 2]) ) lv1: R.Tensor((3, 3, 2, 1), dtype="float32") = R.reshape(lv, R.shape([3, 3, 2, 1])) lv2: R.Tensor((1, 8, 8, 2), dtype="float32") = R.nn.conv2d( data, lv1, strides=[1, 1], padding=[1, 1, 1, 1], dilation=[1, 1], groups=2, data_layout="NHWC", kernel_layout="HWOI", out_layout="NHWC", ) gv: R.Tensor((1, 8, 8, 2), dtype="float32") = R.add( lv2, R.const(np.zeros((2,), dtype="float32")) ) R.output(gv) return gv verify(DepthwiseConv2D, Expected) def test_transpose_conv(): class TransposeConv(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(1, 8, 8, 2), dtype=tf.float32), tf.TensorSpec(shape=(3, 3, 3, 2), dtype=tf.float32), ] ) def func(self, data, kernel): output_shape = tf.constant([1, 8, 8, 3], dtype=tf.int32) return tf.nn.conv2d_transpose( input=data, filters=kernel, output_shape=output_shape, strides=[1, 1, 1, 1], padding="SAME", ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 8, 8, 2), dtype="float32"), kernel: R.Tensor((3, 3, 3, 2), dtype="float32"), ) -> R.Tensor((1, 8, 8, 3), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((3, 3, 3, 2), dtype="float32") = R.permute_dims( kernel, axes=[2, 0, 1, 3] ) lv1: R.Tensor((2, 3, 3, 3), dtype="float32") = R.permute_dims(lv, axes=[3, 0, 1, 2]) gv: R.Tensor((1, 8, 8, 3), dtype="float32") = R.nn.conv2d_transpose( data, lv1, strides=[1, 1], padding=[1, 1, 1, 1], output_padding=[0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="IOHW", out_layout="NHWC", out_dtype="float32", ) R.output(gv) return gv verify(TransposeConv, Expected) def test_l2_pool2d(): class L2Pool2D(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 8, 8, 2), dtype=tf.float32)]) def func(self, data): squared = tf.math.square(data) pooled = tf.nn.avg_pool2d(squared, ksize=[2, 2], strides=[1, 1], padding="SAME") return tf.math.sqrt(pooled) @I.ir_module class Expected: @R.function def main(data: R.Tensor((1, 8, 8, 2), dtype="float32")) -> R.Tensor( (1, 8, 8, 2), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): squared = R.power(data, R.const(2.0, "float32")) pooled = R.nn.avg_pool2d( squared, pool_size=[2, 2], strides=[1, 1], padding=[0, 0, 1, 1], layout="NHWC", ) gv = R.sqrt(pooled) R.output(gv) return gv verify(L2Pool2D, Expected) def test_l2_normalization(): class L2Normalization(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 4), dtype=tf.float32)]) def func(self, x): return tf.nn.l2_normalize(x, axis=-1) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 4), dtype="float32")) -> R.Tensor((2, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 4), dtype="float32") = R.square(x) lv1: R.Tensor((2, 1), dtype="float32") = R.sum(lv, axis=[1], keepdims=True) lv2: R.Tensor((2, 1), dtype="float32") = R.add( lv1, R.const(9.999999960041972e-13, "float32") ) lv3: R.Tensor((2, 1), dtype="float32") = R.sqrt(lv2) gv: R.Tensor((2, 4), dtype="float32") = R.divide(x, lv3) R.output(gv) return gv verify(L2Normalization, Expected) def test_local_response_normalization(): class LocalResponseNormalization(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 8, 8, 4), dtype=tf.float32)]) def func(self, x): return tf.nn.local_response_normalization( x, depth_radius=2, bias=1.0, alpha=1e-4, beta=0.75, ) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 8, 8, 4), dtype="float32")) -> R.Tensor( (1, 8, 8, 4), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 8, 8, 4), dtype="float32") = R.square(x) lv1: R.Tensor((64, 4, 1, 1), dtype="float32") = R.reshape( lv, R.shape([64, 4, 1, 1]) ) lv2: R.Tensor((64, 4, 1, 1), dtype="float32") = R.nn.avg_pool2d( lv1, pool_size=[5, 1], strides=[1, 1], dilation=[1, 1], padding=[2, 0, 2, 0], ceil_mode=False, count_include_pad=True, layout="NHWC", out_layout="NHWC", ) lv3: R.Tensor((1, 8, 8, 4), dtype="float32") = R.reshape(lv2, R.shape([1, 8, 8, 4])) lv4: R.Tensor((1, 8, 8, 4), dtype="float32") = R.multiply( R.const(0.00049999996554106474, "float32"), lv3 ) lv5: R.Tensor((1, 8, 8, 4), dtype="float32") = R.add(R.const(1.0, "float32"), lv4) lv6: R.Tensor((1, 8, 8, 4), dtype="float32") = R.power( lv5, R.const(0.75, "float32") ) gv: R.Tensor((1, 8, 8, 4), dtype="float32") = R.divide(x, lv6) R.output(gv) return gv verify(LocalResponseNormalization, Expected) def test_slice(): class Slice(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(3, 4), dtype=tf.float32)]) def func(self, x): return tf.slice(x, begin=[1, 1], size=[2, 2]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 4), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.strided_slice( x, axes=[0, 1], begin=[1, 1], end=[3, 3] ) R.output(gv) return gv verify(Slice, Expected) def test_strided_slice_stride(): class StridedSliceStride(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4, 6), dtype=tf.float32)]) def func(self, x): return x[0:2, 1:5:2] @I.ir_module class Expected: @R.function def main(x: R.Tensor((4, 6), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.strided_slice( x, axes=[0, 1], begin=[0, 1], end=[2, 5], strides=[1, 2], assume_inbound=False, ) gv: R.Tensor((2, 2), dtype="float32") = R.reshape(lv, R.shape([2, 2])) R.output(gv) return gv verify(StridedSliceStride, Expected) def test_strided_slice_negative_stride(): class StridedSliceNegativeStride(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4,), dtype=tf.float32)]) def func(self, x): return x[::-1] @I.ir_module class Expected: @R.function def main(x: R.Tensor((4,), dtype="float32")) -> R.Tensor((4,), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((4,), dtype="float32") = R.strided_slice( x, axes=[0], begin=[4], end=[-5], strides=[-1], assume_inbound=False ) gv: R.Tensor((4,), dtype="float32") = R.reshape(lv, R.shape([4])) R.output(gv) return gv verify(StridedSliceNegativeStride, Expected) def test_reverse_v2(): class ReverseV2(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.reverse(x, axis=[1]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.flip(x, axis=1) R.output(gv) return gv verify(ReverseV2, Expected) def test_reverse_sequence(): mod = _load_model_from_buffer(_build_tflite_reverse_sequence_model()) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2, 4, 3), dtype="float32"), tvmgen_tensor_1: R.Tensor((2,), dtype="int32"), ) -> R.Tensor((2, 4, 3), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 4, 3), dtype="float32") = R.reverse_sequence( tvmgen_tensor_0, tvmgen_tensor_1, seq_axis=1, batch_axis=0 ) R.output(gv) return gv data = np.arange(24, dtype="float32").reshape((2, 4, 3)) seq_lengths = np.array([1, 3], dtype="int32") expected = data.copy() expected[1, :3, :] = expected[1, :3, :][::-1] ex = tvm.compile(mod, tvm.target.Target("c")) vm = relax.VirtualMachine(ex, tvm.cpu()) vm.set_input("main", data, seq_lengths) vm.invoke_stateful("main") output = vm.get_outputs("main") np.testing.assert_allclose(output.numpy(), expected, rtol=1e-5, atol=1e-5) def test_gather(): class Gather(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3, 4), dtype=tf.float32), tf.TensorSpec(shape=(2,), dtype=tf.int64), ] ) def func(self, x, indices): return tf.gather(x, indices, axis=1) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3, 4), dtype="float32"), indices: R.Tensor((2,), dtype="int64"), ) -> R.Tensor((2, 2, 4), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2,), dtype="int32") = R.astype(indices, dtype="int32") gv: R.Tensor((2, 2, 4), dtype="float32") = R.take(x, lv, axis=1, mode="fast") R.output(gv) return gv verify(Gather, Expected) def test_gather_nd(): class GatherND(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3, 4), dtype=tf.float32), tf.TensorSpec(shape=(2, 2), dtype=tf.int32), ] ) def func(self, x, indices): return tf.gather_nd(x, indices) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3, 4), dtype="float32"), indices: R.Tensor((2, 2), dtype="int32"), ) -> R.Tensor((2, 4), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="int32") = R.permute_dims(indices, axes=[-1, 0]) lv1: R.Tensor((2, 2), dtype="int64") = R.astype(lv, dtype="int64") gv: R.Tensor((2, 4), dtype="float32") = R.gather_nd(x, lv1, batch_dims=0) R.output(gv) return gv verify(GatherND, Expected) def test_squeeze(): mod = _load_model_from_buffer(_build_tflite_squeeze_model()) @I.ir_module class Expected: @R.function def main(tvmgen_tensor_0: R.Tensor((1, 2, 1, 3), dtype="float32")) -> R.Tensor( (2, 3), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.squeeze(tvmgen_tensor_0, axis=[0, 2]) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_unpack(): mod = _load_model_from_buffer(_build_tflite_unpack_model()) @I.ir_module class Expected: @R.function def main(tvmgen_tensor_0: R.Tensor((2, 3, 4), dtype="float32")) -> R.Tuple( R.Tensor((2, 4), dtype="float32"), R.Tensor((2, 4), dtype="float32"), R.Tensor((2, 4), dtype="float32"), ): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple( R.Tensor((2, 1, 4), dtype="float32"), R.Tensor((2, 1, 4), dtype="float32"), R.Tensor((2, 1, 4), dtype="float32"), ) = R.split(tvmgen_tensor_0, indices_or_sections=3, axis=1) lv1: R.Tensor((2, 1, 4), dtype="float32") = lv[0] lv2: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv1, axis=[1]) lv3: R.Tensor((2, 1, 4), dtype="float32") = lv[1] lv4: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv3, axis=[1]) lv5: R.Tensor((2, 1, 4), dtype="float32") = lv[2] lv6: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv5, axis=[1]) gv = (lv2, lv4, lv6) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_zeros_like(): mod = _load_model_from_buffer(_build_tflite_zeros_like_model()) @I.ir_module class Expected: @R.function def main(tvmgen_tensor_0: R.Tensor((2, 3), dtype="float32")) -> R.Tensor( (2, 3), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.zeros_like(tvmgen_tensor_0) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _make_conv2d_module(data_shape, kernel_shape, data_format, strides, padding): class Conv2DModule(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=data_shape, dtype=tf.float32), tf.TensorSpec(shape=kernel_shape, dtype=tf.float32), ] ) def func(self, data, kernel): return tf.nn.conv2d( input=data, filters=kernel, data_format=data_format, strides=strides, padding=padding, ) return Conv2DModule def test_conv2d_same(): Conv2DModule = _make_conv2d_module( (1, 128, 128, 32), (3, 3, 32, 32), "NHWC", (1, 1, 1, 1), "SAME" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 128, 128, 32), dtype="float32"), kernel: R.Tensor((3, 3, 32, 32), dtype="float32"), ) -> R.Tensor((1, 128, 128, 32), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((32, 3, 3, 32), dtype="float32") = R.permute_dims( kernel, axes=[3, 0, 1, 2] ) lv1: R.Tensor((3, 3, 32, 32), dtype="float32") = R.permute_dims( lv, axes=[1, 2, 3, 0] ) lv2: R.Tensor((1, 128, 128, 32), dtype="float32") = R.nn.conv2d( data, lv1, strides=[1, 1], padding=[1, 1, 1, 1], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) gv: R.Tensor((1, 128, 128, 32), dtype="float32") = R.add( lv2, R.const(np.zeros((32,), dtype="float32")) ) R.output(gv) return gv verify(Conv2DModule, Expected) def test_conv2d_valid(): Conv2DModule = _make_conv2d_module( (1, 128, 128, 32), (3, 3, 32, 32), "NHWC", (1, 1, 1, 1), "VALID" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 128, 128, 32), dtype="float32"), kernel: R.Tensor((3, 3, 32, 32), dtype="float32"), ) -> R.Tensor((1, 126, 126, 32), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((32, 3, 3, 32), dtype="float32") = R.permute_dims( kernel, axes=[3, 0, 1, 2] ) lv1: R.Tensor((3, 3, 32, 32), dtype="float32") = R.permute_dims( lv, axes=[1, 2, 3, 0] ) lv2: R.Tensor((1, 126, 126, 32), dtype="float32") = R.nn.conv2d( data, lv1, strides=[1, 1], padding=[0, 0, 0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) gv: R.Tensor((1, 126, 126, 32), dtype="float32") = R.add( lv2, R.const(np.zeros((32,), dtype="float32")) ) R.output(gv) return gv verify(Conv2DModule, Expected) def _make_conv3d_module(data_shape, kernel_shape, strides, padding): class Conv3DModule(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=data_shape, dtype=tf.float32), tf.TensorSpec(shape=kernel_shape, dtype=tf.float32), ] ) def func(self, data, kernel): return tf.nn.conv3d( input=data, filters=kernel, strides=strides, padding=padding, ) return Conv3DModule def test_conv3d_valid(): Conv3DModule = _make_conv3d_module((1, 8, 8, 8, 3), (3, 3, 3, 3, 16), (1, 1, 1, 1, 1), "VALID") @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 8, 8, 8, 3), dtype="float32"), kernel: R.Tensor((3, 3, 3, 3, 16), dtype="float32"), ) -> R.Tensor((1, 6, 6, 6, 16), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((1, 6, 6, 6, 16), dtype="float32") = R.nn.conv3d( data, kernel, strides=[1, 1, 1], padding=[0, 0, 0, 0, 0, 0], dilation=[1, 1, 1], groups=1, data_layout="NDHWC", kernel_layout="DHWIO", out_layout="NDHWC", ) R.output(gv) return gv verify(Conv3DModule, Expected) def test_conv3d_same(): Conv3DModule = _make_conv3d_module((1, 8, 8, 8, 3), (3, 3, 3, 3, 16), (1, 1, 1, 1, 1), "SAME") @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 8, 8, 8, 3), dtype="float32"), kernel: R.Tensor((3, 3, 3, 3, 16), dtype="float32"), ) -> R.Tensor((1, 8, 8, 8, 16), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((1, 8, 8, 8, 16), dtype="float32") = R.nn.conv3d( data, kernel, strides=[1, 1, 1], padding=[1, 1, 1, 1, 1, 1], dilation=[1, 1, 1], groups=1, data_layout="NDHWC", kernel_layout="DHWIO", out_layout="NDHWC", ) R.output(gv) return gv verify(Conv3DModule, Expected) def _make_conv3d_transpose_module(data_shape, kernel_shape, strides, padding): # Compute the expected output_shape for tf.nn.conv3d_transpose. # data_shape: (N, D, H, W, C_in), kernel_shape: (KD, KH, KW, C_out, C_in) # strides: (1, sD, sH, sW, 1) batch = data_shape[0] out_channels = kernel_shape[3] out_spatial = [] for i in range(3): # D, H, W in_size = data_shape[1 + i] k_size = kernel_shape[i] s = strides[1 + i] if padding == "VALID": out_spatial.append((in_size - 1) * s + k_size) else: # SAME out_spatial.append(in_size * s) computed_output_shape = [batch, *out_spatial, out_channels] class Conv3DTransposeModule(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=data_shape, dtype=tf.float32), tf.TensorSpec(shape=kernel_shape, dtype=tf.float32), ] ) def func(self, data, kernel): return tf.nn.conv3d_transpose( input=data, filters=kernel, output_shape=computed_output_shape, strides=strides, padding=padding, ) return Conv3DTransposeModule def test_conv3d_transpose_valid(): Conv3DTransposeModule = _make_conv3d_transpose_module( (1, 8, 8, 8, 3), (3, 3, 3, 8, 3), (1, 1, 1, 1, 1), "VALID" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 8, 8, 8, 3), dtype="float32"), kernel: R.Tensor((3, 3, 3, 8, 3), dtype="float32"), ) -> R.Tensor((1, 10, 10, 10, 8), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((1, 10, 10, 10, 8), dtype="float32") = R.nn.conv3d_transpose( data, kernel, strides=[1, 1, 1], padding=[0, 0, 0, 0, 0, 0], output_padding=[0, 0, 0], dilation=[1, 1, 1], groups=1, data_layout="NDHWC", kernel_layout="DHWOI", out_layout="NDHWC", ) R.output(gv) return gv verify(Conv3DTransposeModule, Expected) def test_conv3d_transpose_same(): Conv3DTransposeModule = _make_conv3d_transpose_module( (1, 8, 8, 8, 3), (3, 3, 3, 8, 3), (1, 1, 1, 1, 1), "SAME" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 8, 8, 8, 3), dtype="float32"), kernel: R.Tensor((3, 3, 3, 8, 3), dtype="float32"), ) -> R.Tensor((1, 8, 8, 8, 8), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((1, 8, 8, 8, 8), dtype="float32") = R.nn.conv3d_transpose( data, kernel, strides=[1, 1, 1], padding=[1, 1, 1, 1, 1, 1], output_padding=[0, 0, 0], dilation=[1, 1, 1], groups=1, data_layout="NDHWC", kernel_layout="DHWOI", out_layout="NDHWC", ) R.output(gv) return gv verify(Conv3DTransposeModule, Expected) def _make_pool2d_module(pool, data_shape, ksize, data_format, strides, padding): class Pool2DModule(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=data_shape, dtype=tf.float32), ] ) def func(self, data): return pool( input=data, ksize=ksize, data_format=data_format, strides=strides, padding=padding, ) return Pool2DModule def test_avg_pool2d_same(): Pool2DModule = _make_pool2d_module( tf.nn.avg_pool2d, (1, 128, 128, 32), (2, 2), "NHWC", (1, 1, 1, 1), "SAME" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 128, 128, 32), dtype="float32"), ) -> R.Tensor((1, 128, 128, 32), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 128, 128, 32), dtype="float32") = R.nn.avg_pool2d( data, pool_size=[2, 2], strides=[1, 1], dilation=[1, 1], padding=[0, 0, 1, 1], ceil_mode=False, count_include_pad=False, layout="NHWC", out_layout="NHWC", ) R.output(gv) return gv verify(Pool2DModule, Expected) def test_avg_pool2d_valid(): Pool2DModule = _make_pool2d_module( tf.nn.avg_pool2d, (1, 128, 128, 32), (2, 2), "NHWC", (1, 1, 1, 1), "VALID" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 128, 128, 32), dtype="float32"), ) -> R.Tensor((1, 127, 127, 32), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 127, 127, 32), dtype="float32") = R.nn.avg_pool2d( data, pool_size=[2, 2], strides=[1, 1], dilation=[1, 1], padding=[0, 0, 0, 0], ceil_mode=False, count_include_pad=False, layout="NHWC", out_layout="NHWC", ) R.output(gv) return gv verify(Pool2DModule, Expected) def test_max_pool2d_same(): Pool2DModule = _make_pool2d_module( tf.nn.max_pool2d, (1, 128, 128, 32), (2, 2), "NHWC", (1, 1, 1, 1), "SAME" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 128, 128, 32), dtype="float32"), ) -> R.Tensor((1, 128, 128, 32), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 128, 128, 32), dtype="float32") = R.nn.max_pool2d( data, pool_size=[2, 2], strides=[1, 1], dilation=[1, 1], padding=[0, 0, 1, 1], ceil_mode=False, layout="NHWC", out_layout="NHWC", ) R.output(gv) return gv verify(Pool2DModule, Expected) def test_max_pool2d_valid(): Pool2DModule = _make_pool2d_module( tf.nn.max_pool2d, (1, 128, 128, 32), (2, 2), "NHWC", (1, 1, 1, 1), "VALID" ) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 128, 128, 32), dtype="float32"), ) -> R.Tensor((1, 127, 127, 32), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 127, 127, 32), dtype="float32") = R.nn.max_pool2d( data, pool_size=[2, 2], strides=[1, 1], dilation=[1, 1], padding=[0, 0, 0, 0], ceil_mode=False, count_include_pad=False, layout="NHWC", out_layout="NHWC", ) R.output(gv) return gv verify(Pool2DModule, Expected) @pytest.mark.parametrize( "net, shape", [ # Limiting the tests for CI (keras_app.Xception, (1, 299, 299, 3)), # (keras_app.VGG16, (1, 224, 224, 3)), # (keras_app.VGG19, (1, 224, 224, 3)), (keras_app.ResNet50, (1, 224, 224, 3)), # (keras_app.ResNet50V2, (1, 224, 224, 3)), # (keras_app.ResNet101, (1, 224, 224, 3)), # (keras_app.ResNet101V2, (1, 224, 224, 3)), # (keras_app.ResNet152, (1, 224, 224, 3)), # (keras_app.ResNet152V2, (1, 224, 224, 3)), (keras_app.InceptionResNetV2, (1, 299, 299, 3)), # (keras_app.MobileNet, (1, 224, 224, 3)), (keras_app.MobileNetV2, (1, 224, 224, 3)), (keras_app.DenseNet121, (1, 224, 224, 3)), # (keras_app.DenseNet169, (1, 224, 224, 3)), # (keras_app.DenseNet201, (1, 224, 224, 3)), (keras_app.NASNetMobile, (1, 224, 224, 3)), # (keras_app.NASNetLarge, (1, 331, 331, 3)), (keras_app.EfficientNetB0, (1, 224, 224, 3)), # (keras_app.EfficientNetB1, (1, 240, 240, 3)), # (keras_app.EfficientNetB2, (1, 260, 260, 3)), # (keras_app.EfficientNetB3, (1, 300, 300, 3)), # (keras_app.EfficientNetB4, (1, 380, 380, 3)), # (keras_app.EfficientNetB5, (1, 456, 456, 3)), # (keras_app.EfficientNetB6, (1, 528, 528, 3)), # (keras_app.EfficientNetB7, (1, 600, 600, 3)), (keras_app.EfficientNetV2B0, (1, 224, 224, 3)), # (keras_app.EfficientNetV2B1, (1, 240, 240, 3)), # (keras_app.EfficientNetV2B2, (1, 260, 260, 3)), # (keras_app.EfficientNetV2B3, (1, 300, 300, 3)), # (keras_app.EfficientNetV2S, (1, 384, 384, 3)), # (keras_app.EfficientNetV2M, (1, 480, 480, 3)), # (keras_app.EfficientNetV2L, (1, 480, 480, 3)), # (keras_app.ConvNeXtTiny, (1, 224, 224, 3)), # (keras_app.ConvNeXtSmall, (1, 224, 224, 3)), # (keras_app.ConvNeXtBase, (1, 224, 224, 3)), # (keras_app.ConvNeXtLarge, (1, 224, 224, 3)), # (keras_app.ConvNeXtXLarge, (1, 224, 224, 3)), ], ) def test_networks(net, shape): # Run network tests only in nightly builds if "CI_ENV_NIGHTLY" not in os.environ: return class NetworkModule(tf.Module): def __init__(self): self.model = net(weights=None, include_top=True) @tf.function def func(self, data): return self.model(data, training=False) model = NetworkModule() concrete_func = model.func.get_concrete_function(tf.TensorSpec(shape=shape, dtype=tf.float32)) mod = _get_mod_from_cfunc(concrete_func) tvm.ir.assert_structural_equal(mod["main"].ret_ty, relax.TensorType((1, 1000), "float32")) verify(concrete_func) def test_broadcast_to(): class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 2), dtype=tf.float32)]) def func(self, x): return tf.broadcast_to(x, [3, 2, 2]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((3, 2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((3, 2, 2), dtype="float32") = R.multiply( x, R.const(np.ones((3, 2, 2), dtype="float32")) ) R.output(gv) return gv verify(Model, Expected) class ModelScalarAndInt(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(), dtype=tf.int32)]) def func(self, x): return tf.broadcast_to(x, [4, 4]) @I.ir_module class ExpectedScalarAndInt: @R.function def main(x: R.Tensor((), dtype="int32")) -> R.Tensor((4, 4), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4, 4), dtype="int32") = R.multiply( x, R.const(np.ones((4, 4), dtype="int32")) ) R.output(gv) return gv verify(ModelScalarAndInt, ExpectedScalarAndInt) def test_embedding_lookup(): class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(3,), dtype=tf.int32)]) def func(self, indices): params = tf.constant([[1, 2], [3, 4], [5, 6]], dtype=tf.float32) return tf.nn.embedding_lookup(params, indices) @I.ir_module class Expected: @R.function def main(indices: R.Tensor((3,), dtype="int32")) -> R.Tensor((3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3,), dtype="int32") = R.astype(indices, dtype="int32") gv: R.Tensor((3, 2), dtype="float32") = R.take( R.const(np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32)), lv, axis=0, mode="fast", ) R.output(gv) return gv verify(Model, Expected) class ModelMultidim(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.int32)]) def func(self, indices): params = tf.constant([[1, 2], [3, 4], [5, 6], [7, 8]], dtype=tf.float32) return tf.nn.embedding_lookup(params, indices) @I.ir_module class ExpectedMultidim: @R.function def main(indices: R.Tensor((2, 3), dtype="int32")) -> R.Tensor((2, 3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 3), dtype="int32") = R.astype(indices, dtype="int32") gv: R.Tensor((2, 3, 2), dtype="float32") = R.take( R.const(np.array([[1, 2], [3, 4], [5, 6], [7, 8]], dtype=np.float32)), lv, axis=0, mode="fast", ) R.output(gv) return gv verify(ModelMultidim, ExpectedMultidim) def test_select_v2(): class Model(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 2), dtype=tf.bool), tf.TensorSpec(shape=(2, 2), dtype=tf.float32), tf.TensorSpec(shape=(2, 2), dtype=tf.float32), ] ) def func(self, condition, x, y): return tf.where(condition, x, y) @I.ir_module class Expected: @R.function def main( condition: R.Tensor((2, 2), dtype="bool"), x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.where(condition, x, y) R.output(gv) return gv verify(Model, Expected) class ModelBroadcasting(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 1), dtype=tf.bool), tf.TensorSpec(shape=(2, 2), dtype=tf.float32), tf.TensorSpec(shape=(), dtype=tf.float32), ] ) def func(self, condition, x, y): return tf.where(condition, x, y) @I.ir_module class ExpectedBroadcasting: @R.function def main( condition: R.Tensor((2, 1), dtype="bool"), x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.where(condition, x, y) R.output(gv) return gv verify(ModelBroadcasting, ExpectedBroadcasting) def test_scatter_nd(): class Model(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(4, 1), dtype=tf.int32), tf.TensorSpec(shape=(4,), dtype=tf.float32), tf.TensorSpec(shape=(1,), dtype=tf.int32), ] ) def func(self, indices, updates, shape): return tf.scatter_nd(indices, updates, shape) @I.ir_module class Expected: @R.function def main( indices: R.Tensor((4, 1), dtype="int32"), updates: R.Tensor((4,), dtype="float32"), shape: R.Tensor((1,), dtype="int32"), ) -> R.Tensor(dtype="float32", ndim=1): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((1,), dtype="int64") = R.astype(shape, dtype="int64") lv1: R.Shape(ndim=1) = R.tensor_to_shape(lv) lv2: R.Tensor(lv1, dtype="float32") = R.zeros(lv1, dtype="float32") lv3: R.Tensor((1, 4), dtype="int32") = R.permute_dims(indices, axes=[-1, 0]) gv: R.Tensor(dtype="float32", ndim=1) = R.scatter_nd( lv2, lv3, updates, reduction="update" ) R.output(gv) return gv verify(Model, Expected) def test_segment_sum(): """SEGMENT_SUM lowers to scatter_nd with add reduction.""" class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4, 2), dtype=tf.float32)]) def func(self, data): return tf.raw_ops.SegmentSum( data=data, segment_ids=tf.constant([0, 0, 1, 2], dtype=tf.int32) ) @I.ir_module class Expected: @R.function def main(data: R.Tensor((4, 2), dtype="float32")) -> R.Tensor((3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 2), dtype="float32") = R.zeros(R.shape([3, 2]), dtype="float32") lv1: R.Tensor((4, 1), dtype="int32") = R.expand_dims( R.const([0, 0, 1, 2], "int32"), axis=[1] ) gv: R.Tensor((3, 2), dtype="float32") = R.scatter_nd(lv, lv1, data, reduction="add") R.output(gv) return gv verify(Model, Expected) def test_unsorted_segment_min(): """UNSORTED_SEGMENT_MIN lowers to scatter_nd with min reduction.""" class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4, 2), dtype=tf.float32)]) def func(self, data): return tf.raw_ops.UnsortedSegmentMin( data=data, segment_ids=tf.constant([2, 0, 2, 1], dtype=tf.int32), num_segments=tf.constant(3, dtype=tf.int32), ) @I.ir_module class Expected: @R.function def main(data: R.Tensor((4, 2), dtype="float32")) -> R.Tensor((3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 2), dtype="float32") = R.full( R.shape([3, 2]), R.const(np.finfo(np.float32).max, "float32"), dtype="float32" ) lv1: R.Tensor((4, 1), dtype="int32") = R.expand_dims( R.const([2, 0, 2, 1], "int32"), axis=[1] ) gv: R.Tensor((3, 2), dtype="float32") = R.scatter_nd(lv, lv1, data, reduction="min") R.output(gv) return gv verify(Model, Expected) def test_unsorted_segment_sum(): """UNSORTED_SEGMENT_SUM lowers to scatter_nd with add reduction.""" class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4, 2), dtype=tf.float32)]) def func(self, data): return tf.raw_ops.UnsortedSegmentSum( data=data, segment_ids=tf.constant([0, 2, 1, 2], dtype=tf.int32), num_segments=tf.constant(3, dtype=tf.int32), ) @I.ir_module class Expected: @R.function def main(data: R.Tensor((4, 2), dtype="float32")) -> R.Tensor((3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 2), dtype="float32") = R.zeros(R.shape([3, 2]), dtype="float32") lv1: R.Tensor((4, 1), dtype="int32") = R.expand_dims( R.const([0, 2, 1, 2], "int32"), axis=[1] ) gv: R.Tensor((3, 2), dtype="float32") = R.scatter_nd(lv, lv1, data, reduction="add") R.output(gv) return gv verify(Model, Expected) def test_unsorted_segment_max(): """UNSORTED_SEGMENT_MAX lowers to scatter_nd with max reduction.""" class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4, 2), dtype=tf.float32)]) def func(self, data): return tf.raw_ops.UnsortedSegmentMax( data=data, segment_ids=tf.constant([0, 2, 1, 2], dtype=tf.int32), num_segments=tf.constant(3, dtype=tf.int32), ) @I.ir_module class Expected: @R.function def main(data: R.Tensor((4, 2), dtype="float32")) -> R.Tensor((3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 2), dtype="float32") = R.full( R.shape([3, 2]), R.const(np.finfo(np.float32).min, "float32"), dtype="float32" ) lv1: R.Tensor((4, 1), dtype="int32") = R.expand_dims( R.const([0, 2, 1, 2], "int32"), axis=[1] ) gv: R.Tensor((3, 2), dtype="float32") = R.scatter_nd(lv, lv1, data, reduction="max") R.output(gv) return gv verify(Model, Expected) def test_unsorted_segment_prod(): """UNSORTED_SEGMENT_PROD lowers to scatter_nd with mul reduction.""" class Model(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(4, 2), dtype=tf.float32)]) def func(self, data): return tf.raw_ops.UnsortedSegmentProd( data=data, segment_ids=tf.constant([1, 0, 1, 2], dtype=tf.int32), num_segments=tf.constant(3, dtype=tf.int32), ) @I.ir_module class Expected: @R.function def main(data: R.Tensor((4, 2), dtype="float32")) -> R.Tensor((3, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 2), dtype="float32") = R.full( R.shape([3, 2]), R.const(1, "float32"), dtype="float32" ) lv1: R.Tensor((4, 1), dtype="int32") = R.expand_dims( R.const([1, 0, 1, 2], "int32"), axis=[1] ) gv: R.Tensor((3, 2), dtype="float32") = R.scatter_nd(lv, lv1, data, reduction="mul") R.output(gv) return gv verify(Model, Expected) def test_batch_matmul(): class BatchMatMul(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3, 4), dtype=tf.float32), tf.TensorSpec(shape=(2, 4, 5), dtype=tf.float32), ] ) def func(self, x, y): return tf.matmul(x, y) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 3, 4), dtype="float32"), y: R.Tensor((2, 4, 5), dtype="float32"), ) -> R.Tensor((2, 3, 5), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2, 3, 5), dtype="float32") = R.matmul(x, y) gv: R.Tensor((2, 3, 5), dtype="float32") = R.reshape(lv, R.shape([2, 3, 5])) R.output(gv) return gv verify(BatchMatMul, Expected) def test_batch_matmul_adj(): class BatchMatMulAdj(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 4, 3), dtype=tf.float32), tf.TensorSpec(shape=(2, 5, 4), dtype=tf.float32), ] ) def func(self, x, y): return tf.matmul(x, y, transpose_a=True, transpose_b=True) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 4, 3), dtype="float32"), y: R.Tensor((2, 5, 4), dtype="float32"), ) -> R.Tensor((2, 3, 5), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2, 3, 4), dtype="float32") = R.permute_dims(x, axes=[0, 2, 1]) lv1: R.Tensor((2, 4, 5), dtype="float32") = R.permute_dims(y, axes=[0, 2, 1]) lv2: R.Tensor((2, 3, 5), dtype="float32") = R.matmul(lv, lv1) gv: R.Tensor((2, 3, 5), dtype="float32") = R.reshape(lv2, R.shape([2, 3, 5])) R.output(gv) return gv verify(BatchMatMulAdj, Expected) def _verify_nms_v4(mod, tf_func, boxes_np, scores_np): """E2E verify for NMS V4: only run on nightly, compare valid outputs only.""" if "CI_ENV_NIGHTLY" not in os.environ: return tf_indices, tf_valid = tf_func(tf.constant(boxes_np), tf.constant(scores_np)) n_valid = int(tf_valid.numpy()) tgt = tvm.target.Target("llvm") ex = tvm.compile(mod, tgt) vm = relax.VirtualMachine(ex, tvm.cpu()) vm.set_input("main", boxes_np, scores_np) vm.invoke_stateful("main") tvm_indices, tvm_valid = vm.get_outputs("main") assert int(tvm_valid.numpy()) == n_valid np.testing.assert_array_equal( tf_indices.numpy()[:n_valid], tvm_indices.numpy()[:n_valid], ) def _build_nms_v4_mod(num_boxes, max_output_size, iou_threshold, score_threshold): """Convert a NonMaxSuppressionV4 TFLite model to a Relax module. Scalar params must be Python literals (not tf.constant) so TFLite can statically infer output shapes during conversion. """ class NMSv4Module(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(num_boxes, 4), dtype=tf.float32), tf.TensorSpec(shape=(num_boxes,), dtype=tf.float32), ] ) def func(self, boxes, scores): indices, valid = tf.raw_ops.NonMaxSuppressionV4( boxes=boxes, scores=scores, max_output_size=max_output_size, iou_threshold=iou_threshold, score_threshold=score_threshold, pad_to_max_output_size=True, ) return indices, valid instance = NMSv4Module() cf = instance.func.get_concrete_function() mod = _get_mod_from_cfunc(cf) return mod, instance.func def _verify_nms_v5(mod, tf_func, boxes_np, scores_np, soft_nms_sigma=0.0): """E2E verify for NMS: only run on nightly, compare valid outputs only.""" if "CI_ENV_NIGHTLY" not in os.environ: return tf_indices, tf_scores, tf_valid = tf_func(tf.constant(boxes_np), tf.constant(scores_np)) n_valid = int(tf_valid.numpy()) tgt = tvm.target.Target("llvm") ex = tvm.compile(mod, tgt) vm = relax.VirtualMachine(ex, tvm.cpu()) vm.set_input("main", boxes_np, scores_np) vm.invoke_stateful("main") tvm_indices, tvm_scores, tvm_valid = vm.get_outputs("main") assert int(tvm_valid.numpy()) == n_valid np.testing.assert_array_equal( tf_indices.numpy()[:n_valid], tvm_indices.numpy()[:n_valid], ) np.testing.assert_allclose( tf_scores.numpy()[:n_valid], tvm_scores.numpy()[:n_valid], rtol=1e-5, atol=1e-5, ) if soft_nms_sigma > 0.0: np.testing.assert_allclose( tf_scores.numpy(), tvm_scores.numpy(), rtol=1e-5, atol=1e-5, ) np.testing.assert_array_less(-1e-6, tvm_scores.numpy()[n_valid:]) def _build_nms_v5_mod( num_boxes, max_output_size, iou_threshold, score_threshold, soft_nms_sigma=0.0 ): """Convert a NonMaxSuppressionV5 TFLite model to a Relax module. Scalar params must be Python literals (not tf.constant) so TFLite can statically infer output shapes during conversion. """ class NMSv5Module(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(num_boxes, 4), dtype=tf.float32), tf.TensorSpec(shape=(num_boxes,), dtype=tf.float32), ] ) def func(self, boxes, scores): indices, out_scores, valid = tf.raw_ops.NonMaxSuppressionV5( boxes=boxes, scores=scores, max_output_size=max_output_size, iou_threshold=iou_threshold, score_threshold=score_threshold, soft_nms_sigma=soft_nms_sigma, pad_to_max_output_size=True, ) return indices, out_scores, valid instance = NMSv5Module() cf = instance.func.get_concrete_function() mod = _get_mod_from_cfunc(cf) return mod, instance.func class _StubDetectionPostprocessTensor: def __init__(self, shape, name): self._shape = list(shape) self._name = name def Shape(self, index): return self._shape[index] def Name(self): return self._name def Type(self): return 0 class _StubDetectionPostprocessOp: def __init__(self, custom_options): self._custom_options = _encode_detection_postprocess_custom_options(custom_options) def CustomOptionsAsNumpy(self): return np.frombuffer(self._custom_options, dtype="uint8") _DETECTION_POSTPROCESS_ANCHORS = np.array( [ [0.5, 0.5, 1.0, 1.0], [0.5, 0.2, 1.0, 1.0], [0.1, 0.1, 0.5, 0.5], [0.8, 0.8, 0.2, 0.2], ], dtype="float32", ) def _encode_detection_postprocess_custom_options(custom_options): from flatbuffers import flexbuffers builder = flexbuffers.Builder() with builder.Map(): for key, value in custom_options.items(): if isinstance(value, bool): builder.Bool(key, value) elif isinstance(value, int): builder.Int(key, value) else: builder.Float(key, float(value)) return bytes(builder.Finish()) def _make_detection_postprocess_tensor_wrapper(tensor_idx, shape, name): return tflite_frontend.TensorWrapper( tensor_idx, _StubDetectionPostprocessTensor(shape, name), None, ) def _build_detection_postprocess_mod( *, num_classes=1, max_detections=4, detections_per_class=4, use_regular_nms=False, nms_iou_threshold=0.5, nms_score_threshold=0.3, x_scale=10.0, y_scale=10.0, w_scale=5.0, h_scale=5.0, batch_size=2, num_anchors=4, input_num_classes=None, ): custom_options = { "num_classes": num_classes, "max_detections": max_detections, "detections_per_class": detections_per_class, "nms_iou_threshold": nms_iou_threshold, "nms_score_threshold": nms_score_threshold, "x_scale": x_scale, "y_scale": y_scale, "w_scale": w_scale, "h_scale": h_scale, "use_regular_nms": use_regular_nms, } return _convert_detection_postprocess_with_options( custom_options, batch_size=batch_size, num_anchors=num_anchors, num_classes=num_classes, input_num_classes=input_num_classes, ) def _convert_detection_postprocess_with_options( custom_options, *, batch_size=2, num_anchors=4, num_classes=1, input_num_classes=None, build_module=True, ): input_num_classes = num_classes if input_num_classes is None else input_num_classes loc = relax.Var("loc", relax.TensorType((batch_size, num_anchors, 4), "float32")) cls = relax.Var( "cls", relax.TensorType((batch_size, num_anchors, input_num_classes), "float32") ) inputs = [ _make_detection_postprocess_tensor_wrapper(0, (batch_size, num_anchors, 4), "loc"), _make_detection_postprocess_tensor_wrapper( 1, (batch_size, num_anchors, input_num_classes), "cls" ), _make_detection_postprocess_tensor_wrapper(2, (num_anchors, 4), "anchors"), ] converter = tflite_frontend.OperatorConverter.__new__(tflite_frontend.OperatorConverter) converter.bb = relax.BlockBuilder() converter.exp_tab = tflite_frontend.ExprTable() converter.get_input_tensors = lambda op: inputs converter.get_expr = lambda tensor_idx: {0: loc, 1: cls}[tensor_idx] converter.get_tensor_value = lambda tensor: ( _DETECTION_POSTPROCESS_ANCHORS if tensor.tensor_idx == 2 else None ) converter.get_tensor_type_str = lambda tensor_type: "float32" op = _StubDetectionPostprocessOp(custom_options) if not build_module: return converter.convert_detection_postprocess(op) bb = converter.bb with bb.function("main", [loc, cls]): with bb.dataflow(): output = converter.convert_detection_postprocess(op) gv = bb.emit_output(output) bb.emit_func_output(gv) return bb.get() def _make_valid_boxes(rng, n): """Generate n random boxes with y1<=y2, x1<=x2 using the given RNG.""" raw = rng.random((n, 4), dtype=np.float32) return np.stack( [ np.minimum(raw[:, 0], raw[:, 2]), # y1 np.minimum(raw[:, 1], raw[:, 3]), # x1 np.maximum(raw[:, 0], raw[:, 2]), # y2 np.maximum(raw[:, 1], raw[:, 3]), # x2 ], axis=1, ).astype(np.float32) _NMS_V5_CASES = [ pytest.param( 6, 3, 0.5, 0.0, np.array( [ [0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.0, 0.1, 1.0, 1.1], [0.0, 0.0, 1.0, 0.9], [0.5, 0.5, 1.5, 1.5], [0.0, 0.0, 0.3, 0.3], ], dtype=np.float32, ), np.array([0.9, 0.75, 0.6, 0.5, 0.4, 0.3], dtype=np.float32), id="basic", ), pytest.param( 8, 4, 0.5, 0.4, _make_valid_boxes(np.random.default_rng(42), 8), np.random.default_rng(42).random(8, dtype=np.float32), id="score_threshold", ), pytest.param( 5, 3, 0.5, 0.99, _make_valid_boxes(np.random.default_rng(0), 5), np.array([0.1, 0.2, 0.3, 0.4, 0.5], dtype=np.float32), id="all_suppressed", ), pytest.param( 6, 6, 0.1, 0.0, np.array( [ [0.0, 0.0, 0.4, 0.4], [0.5, 0.5, 0.9, 0.9], [0.1, 0.1, 0.5, 0.5], [0.6, 0.6, 1.0, 1.0], [0.0, 0.5, 0.4, 0.9], [0.5, 0.0, 0.9, 0.4], ], dtype=np.float32, ), np.array([0.9, 0.85, 0.7, 0.65, 0.6, 0.55], dtype=np.float32), id="iou_threshold", ), pytest.param( 4, 10, 0.5, 0.0, np.array( [ [0.0, 0.0, 0.3, 0.3], [0.5, 0.5, 0.8, 0.8], [0.1, 0.1, 0.4, 0.4], [0.6, 0.6, 0.9, 0.9], ], dtype=np.float32, ), np.array([0.9, 0.85, 0.7, 0.65], dtype=np.float32), id="max_output_size_larger_than_boxes", ), ] _NMS_V5_SOFT_CASES = [ pytest.param( 6, 6, 0.5, 0.0, 0.5, np.array( [ [0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.0, 0.1, 1.0, 1.1], [0.0, 0.0, 1.0, 0.9], [0.5, 0.5, 1.5, 1.5], [0.0, 0.0, 0.3, 0.3], ], dtype=np.float32, ), np.array([0.9, 0.75, 0.6, 0.5, 0.4, 0.3], dtype=np.float32), id="soft_nms_basic", ), pytest.param( 5, 5, 0.5, 0.0, 0.3, np.array( [ [0.0, 0.0, 1.0, 1.0], [0.1, 0.1, 1.1, 1.1], [0.2, 0.2, 1.2, 1.2], [0.3, 0.3, 1.3, 1.3], [2.0, 2.0, 3.0, 3.0], ], dtype=np.float32, ), np.array([0.9, 0.8, 0.7, 0.6, 0.5], dtype=np.float32), id="soft_nms_tight_sigma", ), pytest.param( 3, 3, 0.5, 0.3, 0.1, np.array( [ [0.0, 0.0, 1.0, 1.0], [0.2, 0.2, 1.2, 1.2], [2.0, 2.0, 3.0, 3.0], ], dtype=np.float32, ), np.array([0.9, 0.8, 0.75], dtype=np.float32), id="soft_nms_threshold_hole", ), pytest.param( 3, 3, 0.5, 0.0, 0.1, np.array( [ [0.0, 0.0, 1.0, 1.0], [0.2, 0.2, 1.2, 1.2], [2.0, 2.0, 3.0, 3.0], ], dtype=np.float32, ), np.array([0.9, 0.85, 0.8], dtype=np.float32), id="soft_nms_reorder", ), ] @pytest.mark.parametrize( "num_boxes,max_output_size,iou_threshold,score_threshold,boxes,scores", _NMS_V5_CASES, ) def test_nms_v5(num_boxes, max_output_size, iou_threshold, score_threshold, boxes, scores): """NON_MAX_SUPPRESSION_V5: conversion smoke test + E2E correctness (nightly only).""" mod, tf_func = _build_nms_v5_mod(num_boxes, max_output_size, iou_threshold, score_threshold) _verify_nms_v5(mod, tf_func, boxes, scores) @pytest.mark.parametrize( "num_boxes,max_output_size,iou_threshold,score_threshold,soft_nms_sigma,boxes,scores", _NMS_V5_SOFT_CASES, ) def test_nms_v5_soft( num_boxes, max_output_size, iou_threshold, score_threshold, soft_nms_sigma, boxes, scores ): """NON_MAX_SUPPRESSION_V5 with soft_nms_sigma: conversion smoke test + E2E correctness.""" mod, tf_func = _build_nms_v5_mod( num_boxes, max_output_size, iou_threshold, score_threshold, soft_nms_sigma ) _verify_nms_v5(mod, tf_func, boxes, scores, soft_nms_sigma=soft_nms_sigma) def test_nms_v5_ir(): """Verify the emitted Relax IR has correct structure for NON_MAX_SUPPRESSION_V5.""" num_boxes = 6 max_output_size = 3 mod, _ = _build_nms_v5_mod( num_boxes=num_boxes, max_output_size=max_output_size, iou_threshold=0.5, score_threshold=0.0, ) tvm.ir.assert_structural_equal( mod["main"].ret_ty, relax.TupleType( [ relax.TensorType((max_output_size,), "int32"), relax.TensorType((max_output_size,), "float32"), relax.TensorType((), "int32"), ] ), ) def test_nms_v5_soft_ir(): """Verify the emitted Relax IR passes soft_nms_sigma for NON_MAX_SUPPRESSION_V5.""" num_boxes = 6 max_output_size = 3 mod, _ = _build_nms_v5_mod( num_boxes=num_boxes, max_output_size=max_output_size, iou_threshold=0.5, score_threshold=0.0, soft_nms_sigma=0.5, ) tvm.ir.assert_structural_equal( mod["main"].ret_ty, relax.TupleType( [ relax.TensorType((max_output_size,), "int32"), relax.TensorType((max_output_size,), "float32"), relax.TensorType((), "int32"), ] ), ) _NMS_V4_CASES = [ pytest.param( 6, 3, 0.5, 0.0, np.array( [ [0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.0, 0.1, 1.0, 1.1], [0.0, 0.0, 1.0, 0.9], [0.5, 0.5, 1.5, 1.5], [0.0, 0.0, 0.3, 0.3], ], dtype=np.float32, ), np.array([0.9, 0.75, 0.6, 0.5, 0.4, 0.3], dtype=np.float32), id="basic", ), pytest.param( 8, 4, 0.5, 0.4, _make_valid_boxes(np.random.default_rng(42), 8), np.random.default_rng(42).random(8, dtype=np.float32), id="score_threshold", ), pytest.param( 5, 3, 0.5, 0.99, _make_valid_boxes(np.random.default_rng(0), 5), np.array([0.1, 0.2, 0.3, 0.4, 0.5], dtype=np.float32), id="all_suppressed", ), pytest.param( 4, 10, 0.5, 0.0, np.array( [ [0.0, 0.0, 0.3, 0.3], [0.5, 0.5, 0.8, 0.8], [0.1, 0.1, 0.4, 0.4], [0.6, 0.6, 0.9, 0.9], ], dtype=np.float32, ), np.array([0.9, 0.85, 0.7, 0.65], dtype=np.float32), id="max_output_size_larger_than_boxes", ), ] @pytest.mark.parametrize( "num_boxes,max_output_size,iou_threshold,score_threshold,boxes,scores", _NMS_V4_CASES, ) def test_nms_v4(num_boxes, max_output_size, iou_threshold, score_threshold, boxes, scores): """NON_MAX_SUPPRESSION_V4: conversion smoke test + E2E correctness (nightly only).""" mod, tf_func = _build_nms_v4_mod(num_boxes, max_output_size, iou_threshold, score_threshold) _verify_nms_v4(mod, tf_func, boxes, scores) def test_nms_v4_ir(): """Verify the emitted Relax IR has correct structure for NON_MAX_SUPPRESSION_V4.""" num_boxes = 6 max_output_size = 3 mod, _ = _build_nms_v4_mod( num_boxes=num_boxes, max_output_size=max_output_size, iou_threshold=0.5, score_threshold=0.0, ) tvm.ir.assert_structural_equal( mod["main"].ret_ty, relax.TupleType( [ relax.TensorType((max_output_size,), "int32"), relax.TensorType((), "int32"), ] ), ) _DETECTION_POSTPROCESS_SMOKE_CASES = [ pytest.param( { "num_classes": 2, "input_num_classes": 3, "max_detections": 2, "detections_per_class": 2, "use_regular_nms": False, "nms_iou_threshold": 0.5, "nms_score_threshold": 0.5, "batch_size": 1, "num_anchors": 4, }, 2, False, id="basic_fast_nms", ), pytest.param( { "num_classes": 2, "input_num_classes": 3, "max_detections": 3, "detections_per_class": 2, "use_regular_nms": True, "nms_iou_threshold": 0.45, "nms_score_threshold": 0.25, "batch_size": 2, "num_anchors": 4, }, 1, True, id="regular_nms_multi_batch", ), ] _DETECTION_POSTPROCESS_SHAPE_CASES = [ pytest.param( { "num_classes": 2, "input_num_classes": 5, "max_detections": 2, "detections_per_class": 2, "use_regular_nms": False, "nms_iou_threshold": 0.5, "nms_score_threshold": 0.5, "batch_size": 1, "num_anchors": 4, }, id="wider_input_classes", ), pytest.param( { "num_classes": 2, "input_num_classes": 3, "max_detections": 4, "detections_per_class": 4, "use_regular_nms": False, "nms_iou_threshold": 0.5, "nms_score_threshold": 0.5, "batch_size": 1, "num_anchors": 4, }, id="larger_max_detections", ), ] @pytest.mark.parametrize( "build_kwargs,expected_topk_count,expected_keep_background", _DETECTION_POSTPROCESS_SMOKE_CASES, ) def test_detection_postprocess_smoke(build_kwargs, expected_topk_count, expected_keep_background): mod = _build_detection_postprocess_mod(**build_kwargs) expected_batch = build_kwargs["batch_size"] expected_max_detections = build_kwargs["max_detections"] tvm.ir.assert_structural_equal( mod["main"].ret_ty, relax.TupleType( [ relax.TensorType((expected_batch, expected_max_detections, 4), "float32"), relax.TensorType((expected_batch, expected_max_detections), "float32"), relax.TensorType((expected_batch, expected_max_detections), "float32"), relax.TensorType((expected_batch,), "float32"), ] ), ) legalized = relax.transform.LegalizeOps()(mod) tvm.ir.assert_structural_equal(legalized["main"].ret_ty, mod["main"].ret_ty) @pytest.mark.parametrize("build_kwargs", _DETECTION_POSTPROCESS_SHAPE_CASES) def test_detection_postprocess_shape_variations(build_kwargs): mod = _build_detection_postprocess_mod(**build_kwargs) batch_size = build_kwargs["batch_size"] num_anchors = build_kwargs["num_anchors"] input_num_classes = build_kwargs["input_num_classes"] max_detections = build_kwargs["max_detections"] tvm.ir.assert_structural_equal( mod["main"].params[1].ty, relax.TensorType((batch_size, num_anchors, input_num_classes), "float32"), ) tvm.ir.assert_structural_equal( mod["main"].ret_ty, relax.TupleType( [ relax.TensorType((batch_size, max_detections, 4), "float32"), relax.TensorType((batch_size, max_detections), "float32"), relax.TensorType((batch_size, max_detections), "float32"), relax.TensorType((batch_size,), "float32"), ] ), ) def _make_resize_expected( input_shape, output_size, method, coordinate_transformation_mode, rounding_method ): """Build an Expected IRModule programmatically to avoid TVMScript variable scope limitations.""" bb = relax.BlockBuilder() x = relax.Var("x", relax.TensorType(input_shape, "float32")) with bb.function("main", [x]): with bb.dataflow(): gv = bb.emit_output( relax.op.image.resize2d( x, size=relax.ShapeExpr([output_size[0], output_size[1]]), roi=[0.0, 0.0, 0.0, 0.0], layout="NHWC", method=method, coordinate_transformation_mode=coordinate_transformation_mode, rounding_method=rounding_method, cubic_alpha=-0.75, cubic_exclude=0, extrapolation_value=0.0, ) ) bb.emit_func_output(gv) mod = bb.get() mod["main"] = mod["main"].with_attr("num_input", 1) return mod @pytest.mark.parametrize( "input_shape, output_size, tf_op, coordinate_transformation_mode", [ ( (1, 4, 4, 1), [8, 8], lambda x: tf.image.resize(x, [8, 8], method="bilinear"), "half_pixel", ), ( (1, 8, 8, 3), [4, 4], lambda x: tf.image.resize(x, [4, 4], method="bilinear"), "half_pixel", ), ( (1, 4, 4, 1), [7, 7], lambda x: tf.compat.v1.image.resize_bilinear(x, [7, 7], align_corners=True), "align_corners", ), ( (1, 4, 4, 2), [8, 8], lambda x: tf.compat.v1.image.resize_bilinear(x, [8, 8], half_pixel_centers=True), "half_pixel", ), ( (2, 6, 6, 16), [12, 12], lambda x: tf.image.resize(x, [12, 12], method="bilinear"), "half_pixel", ), ( (1, 5, 5, 3), [5, 5], lambda x: tf.image.resize(x, [5, 5], method="bilinear"), "half_pixel", ), ( (1, 4, 8, 1), [8, 16], lambda x: tf.image.resize(x, [8, 16], method="bilinear"), "half_pixel", ), ], ) def test_resize_bilinear(input_shape, output_size, tf_op, coordinate_transformation_mode): class ResizeBilinear(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=tf.float32)]) def func(self, x): return tf_op(x) expected = _make_resize_expected( input_shape, output_size, "linear", coordinate_transformation_mode, "" ) verify(ResizeBilinear, expected) @pytest.mark.parametrize( "input_shape, output_size, tf_op, coordinate_transformation_mode, rounding_method", [ ( (1, 2, 2, 1), [4, 4], lambda x: tf.image.resize(x, [4, 4], method="nearest"), "half_pixel", "round_prefer_ceil", ), ( (1, 8, 8, 3), [4, 4], lambda x: tf.image.resize(x, [4, 4], method="nearest"), "half_pixel", "round_prefer_ceil", ), ( (1, 4, 4, 1), [7, 7], lambda x: tf.compat.v1.image.resize_nearest_neighbor(x, [7, 7], align_corners=True), "align_corners", "", ), ( (4, 3, 3, 8), [6, 6], lambda x: tf.image.resize(x, [6, 6], method="nearest"), "half_pixel", "round_prefer_ceil", ), ( (1, 4, 8, 1), [8, 16], lambda x: tf.image.resize(x, [8, 16], method="nearest"), "half_pixel", "round_prefer_ceil", ), ( (1, 3, 3, 2), [3, 3], lambda x: tf.image.resize(x, [3, 3], method="nearest"), "half_pixel", "round_prefer_ceil", ), ], ) def test_resize_nearest_neighbor( input_shape, output_size, tf_op, coordinate_transformation_mode, rounding_method ): class ResizeNearest(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=tf.float32)]) def func(self, x): return tf_op(x) expected = _make_resize_expected( input_shape, output_size, "nearest_neighbor", coordinate_transformation_mode, rounding_method, ) verify(ResizeNearest, expected) def _make_reduce_expected(relax_op, input_shape, axes, keepdims, dtype): if axes is None: axes = list(range(len(input_shape))) bb = relax.BlockBuilder() x = relax.Var("x", relax.TensorType(input_shape, dtype)) with bb.function("main", [x]): with bb.dataflow(): gv = bb.emit_output(relax_op(x, axis=axes, keepdims=keepdims)) bb.emit_func_output(gv) mod = bb.get() mod["main"] = mod["main"].with_attr("num_input", 1) return mod @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.reduce_sum, relax.op.sum), (tf.reduce_mean, relax.op.mean), (tf.reduce_max, relax.op.max), (tf.reduce_min, relax.op.min), (tf.reduce_prod, relax.op.prod), ], ) @pytest.mark.parametrize( "input_shape, axes", [ ((1, 8, 8, 3), 1), ((1, 8, 8, 3), [1, 2]), ((1, 8, 8, 3), -1), ((1, 8, 8, 3), None), ((30,), 0), ((2, 5, 2), [0, 2]), ], ) @pytest.mark.parametrize("keepdims", [True, False]) @pytest.mark.parametrize("dtype", [tf.float32, tf.int32]) def test_reduction_ops(tf_op, relax_op, input_shape, axes, keepdims, dtype): class ReduceModule(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=dtype)]) def func(self, x): return tf_op(x, axis=axes, keepdims=keepdims) relax_dtype = "float32" if dtype == tf.float32 else "int32" expected = _make_reduce_expected(relax_op, input_shape, axes, keepdims, relax_dtype) verify(ReduceModule, expected) def _make_reduce_bool_expected(relax_op, input_shape, axes, keepdims): if axes is None: axes = list(range(len(input_shape))) bb = relax.BlockBuilder() x = relax.Var("x", relax.TensorType(input_shape, "bool")) with bb.function("main", [x]): with bb.dataflow(): cast_in = bb.emit(relax.op.astype(x, "int8")) reduced = bb.emit(relax_op(cast_in, axis=axes, keepdims=keepdims)) gv = bb.emit_output(relax.op.astype(reduced, "bool")) bb.emit_func_output(gv) mod = bb.get() mod["main"] = mod["main"].with_attr("num_input", 1) return mod @pytest.mark.parametrize( "tf_op, relax_op", [ (tf.reduce_any, relax.op.max), (tf.reduce_all, relax.op.min), ], ) @pytest.mark.parametrize( "input_shape, axes", [ ((1, 8, 8, 3), 1), ((1, 8, 8, 3), [1, 2]), ((1, 8, 8, 3), -1), ((1, 8, 8, 3), None), ((30,), 0), ((2, 5, 2), [0, 2]), ], ) @pytest.mark.parametrize("keepdims", [True, False]) def test_reduction_bool_ops(tf_op, relax_op, input_shape, axes, keepdims): class ReduceBoolModule(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=tf.bool)]) def func(self, x): return tf_op(x, axis=axes, keepdims=keepdims) expected = _make_reduce_bool_expected(relax_op, input_shape, axes, keepdims) verify(ReduceBoolModule, expected) # Regression guard: compile to catch a bool max/min lowering path. tvm.compile(expected, tvm.target.Target("llvm")) def test_pad(): class Pad(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.pad(x, [[1, 1], [2, 2]]) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((4, 7), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4, 7), dtype="float32") = R.nn.pad( x, pad_width=[1, 1, 2, 2], pad_value=0.0, pad_mode="constant" ) R.output(gv) return gv verify(Pad, Expected) def test_pad_v2(): class PadV2(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 3), dtype=tf.float32)]) def func(self, x): return tf.pad(x, [[1, 1], [2, 2]], constant_values=5.0) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((4, 7), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4, 7), dtype="float32") = R.nn.pad( x, pad_width=[1, 1, 2, 2], pad_value=5.0, pad_mode="constant" ) R.output(gv) return gv verify(PadV2, Expected) def test_mirror_pad(): class MirrorPad(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(3, 4), dtype=tf.float32)]) def func(self, x): return tf.pad(x, [[1, 1], [2, 2]], mode="REFLECT") @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 4), dtype="float32")) -> R.Tensor((5, 8), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((5, 8), dtype="float32") = R.nn.pad( x, pad_width=[1, 1, 2, 2], pad_value=0.0, pad_mode="reflect" ) R.output(gv) return gv verify(MirrorPad, Expected) def test_topk_v2(): class TopKV2(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(5,), dtype=tf.float32)]) def func(self, x): return tf.math.top_k(x, k=3).values @I.ir_module class Expected: @R.function def main(x: R.Tensor((5,), dtype="float32")) -> R.Tensor((3,), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tuple(R.Tensor((3,), dtype="float32"), R.Tensor((3,), dtype="int32")) = ( R.topk(x, k=3, axis=-1, ret_type="both", largest=True, dtype="int32") ) gv: R.Tensor((3,), dtype="float32") = lv[0] R.output(gv) return gv verify(TopKV2, Expected) def test_one_hot(): class OneHot(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(3,), dtype=tf.int32)]) def func(self, x): return tf.one_hot(x, depth=4) @I.ir_module class Expected: @R.function def main(x: R.Tensor((3,), dtype="int32")) -> R.Tensor((3, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((3, 4), dtype="float32") = R.one_hot( x, R.prim_value(T.float32(1.0)), R.prim_value(T.float32(0.0)), depth=4, axis=-1, ) R.output(gv) return gv verify(OneHot, Expected) def test_select(): class Select(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2, 3), dtype=tf.bool), tf.TensorSpec(shape=(2, 3), dtype=tf.float32), tf.TensorSpec(shape=(2, 3), dtype=tf.float32), ] ) def func(self, cond, x, y): return tf.where(cond, x, y) @I.ir_module class Expected: @R.function def main( cond: R.Tensor((2, 3), dtype="bool"), x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32"), ) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.where(cond, x, y) R.output(gv) return gv verify(Select, Expected) def test_depth_to_space(): class DepthToSpace(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 2, 4, 8), dtype=tf.float32)]) def func(self, x): return tf.nn.depth_to_space(x, block_size=2) @I.ir_module class Expected: @R.function def main( x: R.Tensor((1, 2, 4, 8), dtype="float32"), ) -> R.Tensor((1, 4, 8, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 2, 4, 2, 2, 2), dtype="float32") = R.reshape( x, R.shape([1, 2, 4, 2, 2, 2]) ) lv1: R.Tensor((1, 2, 2, 4, 2, 2), dtype="float32") = R.permute_dims( lv, axes=[0, 1, 3, 2, 4, 5] ) gv: R.Tensor((1, 4, 8, 2), dtype="float32") = R.reshape(lv1, R.shape([1, 4, 8, 2])) R.output(gv) return gv verify(DepthToSpace, Expected) def test_space_to_depth(): class SpaceToDepth(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 4, 4, 2), dtype=tf.float32)]) def func(self, x): return tf.nn.space_to_depth(x, block_size=2) @I.ir_module class Expected: @R.function def main( x: R.Tensor((1, 4, 4, 2), dtype="float32"), ) -> R.Tensor((1, 2, 2, 8), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 2, 2, 2, 2, 2), dtype="float32") = R.reshape( x, R.shape([1, 2, 2, 2, 2, 2]) ) lv1: R.Tensor((1, 2, 2, 2, 2, 2), dtype="float32") = R.permute_dims( lv, axes=[0, 1, 3, 2, 4, 5] ) gv: R.Tensor((1, 2, 2, 8), dtype="float32") = R.reshape(lv1, R.shape([1, 2, 2, 8])) R.output(gv) return gv verify(SpaceToDepth, Expected) @pytest.mark.parametrize( "input_shape, block_shape, paddings, expected_out_shape", [ ((1, 2, 2, 1), [2, 2], [[0, 0], [0, 0]], (4, 1, 1, 1)), ((1, 2, 3, 1), [2, 2], [[0, 0], [1, 0]], (4, 1, 2, 1)), ], ) def test_space_to_batch_nd(input_shape, block_shape, paddings, expected_out_shape): """SPACE_TO_BATCH_ND imports to Relax and preserves expected output shape.""" class SpaceToBatchND(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=tf.float32)]) def func(self, x): return tf.space_to_batch_nd( x, tf.constant(block_shape, dtype=tf.int32), tf.constant(paddings, dtype=tf.int32), ) if expected_out_shape == (4, 1, 1, 1): @I.ir_module class ExpectedSpaceToBatchNoPadding: @R.function def main(x: R.Tensor((1, 2, 2, 1), dtype="float32")) -> R.Tensor( (4, 1, 1, 1), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv = R.call_dps_packed( "topi.nn.space_to_batch_nd", ( x, R.shape([2, 2]), R.shape([0, 0]), R.shape([0, 0]), R.prim_value(T.float64(0.0)), ), out_ty=R.Tensor((4, 1, 1, 1), dtype="float32"), ) R.output(gv) return gv expected = ExpectedSpaceToBatchNoPadding else: @I.ir_module class ExpectedSpaceToBatchWithPadding: @R.function def main(x: R.Tensor((1, 2, 3, 1), dtype="float32")) -> R.Tensor( (4, 1, 2, 1), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv = R.call_dps_packed( "topi.nn.space_to_batch_nd", ( x, R.shape([2, 2]), R.shape([0, 1]), R.shape([0, 0]), R.prim_value(T.float64(0.0)), ), out_ty=R.Tensor((4, 1, 2, 1), dtype="float32"), ) R.output(gv) return gv expected = ExpectedSpaceToBatchWithPadding verify(SpaceToBatchND, expected) @pytest.mark.parametrize( "input_shape, block_shape, crops, expected_out_shape", [ ((4, 1, 1, 1), [2, 2], [[0, 0], [0, 0]], (1, 2, 2, 1)), ((4, 1, 2, 1), [2, 2], [[0, 0], [1, 0]], (1, 2, 3, 1)), ], ) def test_batch_to_space_nd(input_shape, block_shape, crops, expected_out_shape): """BATCH_TO_SPACE_ND imports to Relax and preserves expected output shape.""" class BatchToSpaceND(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=input_shape, dtype=tf.float32)]) def func(self, x): return tf.raw_ops.BatchToSpaceND( input=x, block_shape=tf.constant(block_shape, dtype=tf.int32), crops=tf.constant(crops, dtype=tf.int32), ) if expected_out_shape == (1, 2, 2, 1): @I.ir_module class ExpectedBatchToSpaceNoCrop: @R.function def main(x: R.Tensor((4, 1, 1, 1), dtype="float32")) -> R.Tensor( (1, 2, 2, 1), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv = R.call_dps_packed( "topi.nn.batch_to_space_nd", (x, R.shape([2, 2]), R.shape([0, 0]), R.shape([0, 0])), out_ty=R.Tensor((1, 2, 2, 1), dtype="float32"), ) R.output(gv) return gv expected = ExpectedBatchToSpaceNoCrop else: @I.ir_module class ExpectedBatchToSpaceWithCrop: @R.function def main(x: R.Tensor((4, 1, 2, 1), dtype="float32")) -> R.Tensor( (1, 2, 3, 1), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv = R.call_dps_packed( "topi.nn.batch_to_space_nd", (x, R.shape([2, 2]), R.shape([0, 1]), R.shape([0, 0])), out_ty=R.Tensor((1, 2, 3, 1), dtype="float32"), ) R.output(gv) return gv expected = ExpectedBatchToSpaceWithCrop verify(BatchToSpaceND, expected) def test_leaky_relu(): class LeakyReLU(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.nn.leaky_relu(x, alpha=0.2) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="float32") = R.nn.leakyrelu( x, alpha=0.20000000298023224 ) R.output(gv) return gv verify(LeakyReLU, Expected) def test_hard_swish(): class HardSwish(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return x * tf.nn.relu6(x + 3) / 6 @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 30), dtype="float32") = R.add(x, R.const(3.0, dtype="float32")) lv1: R.Tensor((1, 30), dtype="float32") = R.clip( lv, R.prim_value(T.float64(0.0)), R.prim_value(T.float64(6.0)) ) lv2: R.Tensor((1, 30), dtype="float32") = R.multiply(x, lv1) gv: R.Tensor((1, 30), dtype="float32") = R.divide( lv2, R.const(6.0, dtype="float32") ) R.output(gv) return gv verify(HardSwish, Expected) def _build_relu_0_to_1_model(): """Build a minimal TFLite RELU_0_TO_1 model.""" builder = flatbuffers.Builder(1024) builtin_op = _get_builtin_operator("RELU_0_TO_1") op_code = _build_operator_code(builder, builtin_op) tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, [2, 2]), ] op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0], outputs=[1]) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=[_build_buffer(builder), _build_buffer(builder)], ) def test_relu_0_to_1(): """RELU_0_TO_1 lowers to clip(0, 1).""" mod = _load_model_from_buffer(_build_relu_0_to_1_model()) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.clip(x, min=0, max=1) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_relu_n1_to_1(): class ReLU_N1_to_1(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return tf.clip_by_value(x, -1.0, 1.0) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 30), dtype="float32") = R.clip(x, min=-1, max=1) R.output(gv) return gv verify(ReLU_N1_to_1, Expected) def _build_fake_quant_model(*, narrow_range, num_bits=8, min_value=-1.0, max_value=1.0): """Build a minimal TFLite FAKE_QUANT model.""" fake_quant_options = _get_tflite_schema_module("FakeQuantOptions") builder = flatbuffers.Builder(1024) builtin_op = _get_builtin_operator("FAKE_QUANT") op_code = _build_operator_code(builder, builtin_op) fake_quant_options.FakeQuantOptionsStart(builder) fake_quant_options.FakeQuantOptionsAddMin(builder, min_value) fake_quant_options.FakeQuantOptionsAddMax(builder, max_value) fake_quant_options.FakeQuantOptionsAddNumBits(builder, num_bits) fake_quant_options.FakeQuantOptionsAddNarrowRange(builder, narrow_range) options = fake_quant_options.FakeQuantOptionsEnd(builder) tensors = [ _build_tensor(builder, 0, [4]), _build_tensor(builder, 1, [4]), ] op = _build_operator( builder, 0, [0], [1], builtin_options_type=_get_builtin_options_type("FakeQuantOptions"), builtin_options=options, ) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0], outputs=[1]) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=[_build_buffer(builder), _build_buffer(builder)], ) def _fake_quant_reference(data, *, narrow_range, num_bits=8, min_value=-1.0, max_value=1.0): quant_min = 1 if narrow_range else 0 quant_max = (1 << num_bits) - 1 scale = (max_value - min_value) / (quant_max - quant_min) zero_point_from_min = quant_min - min_value / scale if zero_point_from_min <= quant_min: nudged_zero_point = quant_min elif zero_point_from_min >= quant_max: nudged_zero_point = quant_max else: nudged_zero_point = round(zero_point_from_min) nudged_min = (quant_min - nudged_zero_point) * scale nudged_max = (quant_max - nudged_zero_point) * scale clamped = np.clip(data, nudged_min, nudged_max) return np.floor((clamped - nudged_min) / scale + 0.5) * scale + nudged_min def test_fake_quant_narrow_range_vector(): """FAKE_QUANT supports narrow_range on vector inputs.""" mod = _load_model_from_buffer(_build_fake_quant_model(narrow_range=True)) data = np.array([-2.0, -0.5, 0.5, 2.0], dtype=np.float32) output = _run_module(mod, data) expected = _fake_quant_reference(data, narrow_range=True).astype(np.float32) np.testing.assert_allclose(output, expected, rtol=1e-6, atol=1e-6) def test_prelu_basic(): alpha = np.linspace(0.1, 0.3, 30, dtype=np.float32) alpha_init = tf.keras.initializers.Constant(alpha) prelu = tf.keras.layers.PReLU(alpha_initializer=alpha_init) class TfInput(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(1, 30), dtype=tf.float32)]) def func(self, x): return prelu(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 30), dtype="float32")) -> R.Tensor((1, 30), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 30), dtype="float32") = R.broadcast_to( R.const(alpha), R.shape([1, 30]) ) lv1: R.Tensor((30,), dtype="float32") = R.reshape(x, R.shape([30])) lv2: R.Tensor((30,), dtype="float32") = R.reshape(lv, R.shape([30])) lv3: R.Tensor((30,), dtype="float32") = R.nn.prelu(lv1, lv2, axis=0) gv: R.Tensor((1, 30), dtype="float32") = R.reshape(lv3, R.shape([1, 30])) R.output(gv) return gv verify(TfInput, Expected) @pytest.mark.parametrize( "shared_axes", [ pytest.param([1, 2], id="channelwise_shared_axes"), pytest.param([1, 2, 3], id="scalar_shared_axes"), pytest.param(None, id="elementwise_no_shared_axes"), ], ) def test_prelu(shared_axes): inputs = tf.keras.Input(shape=(4, 4, 3), batch_size=1, dtype=tf.float32) prelu_kwargs = { "alpha_initializer": tf.initializers.constant(0.25), } if shared_axes is not None: prelu_kwargs["shared_axes"] = shared_axes outputs = tf.keras.layers.PReLU(**prelu_kwargs)(inputs) keras_model = tf.keras.Model(inputs, outputs) converter = tf.lite.TFLiteConverter.from_keras_model(keras_model) tflite_model_buf = converter.convert() if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(tflite_model_buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(tflite_model_buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") if shared_axes == [1, 2]: alpha_const = np.full((1, 1, 3), 0.25, dtype=np.float32) elif shared_axes == [1, 2, 3]: alpha_const = np.full((1, 1, 1), 0.25, dtype=np.float32) else: alpha_const = np.full((4, 4, 3), 0.25, dtype=np.float32) @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 4, 4, 3), dtype="float32")) -> R.Tensor( (1, 4, 4, 3), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 4, 4, 3), dtype="float32") = R.broadcast_to( R.const(alpha_const), R.shape([1, 4, 4, 3]) ) lv1: R.Tensor((48,), dtype="float32") = R.reshape(x, R.shape([48])) lv2: R.Tensor((48,), dtype="float32") = R.reshape(lv, R.shape([48])) lv3: R.Tensor((48,), dtype="float32") = R.nn.prelu(lv1, lv2, axis=0) gv: R.Tensor((1, 4, 4, 3), dtype="float32") = R.reshape(lv3, R.shape([1, 4, 4, 3])) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_matrix_diag(): """Test TFLite MATRIX_DIAG operator.""" class MatrixDiag(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(3,), dtype=tf.float32)]) def func(self, diagonal): return tf.raw_ops.MatrixDiag(diagonal=diagonal) @I.ir_module class Expected: @R.function def main(diagonal: R.Tensor((3,), dtype="float32")) -> R.Tensor((3, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 3), dtype="float32") = R.zeros(R.shape([3, 3]), dtype="float32") gv = R.call_dps_packed( "topi.matrix_set_diag", ( lv, diagonal, R.const(0, "int32"), R.const(0, "int32"), R.const(False, "bool"), R.const(False, "bool"), ), out_ty=R.Tensor((3, 3), dtype="float32"), ) R.output(gv) return gv verify(MatrixDiag, Expected) def test_matrix_set_diag(): """Test TFLite MATRIX_SET_DIAG operator.""" class MatrixSetDiag(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(3, 3), dtype=tf.float32), tf.TensorSpec(shape=(3,), dtype=tf.float32), ] ) def func(self, input, diagonal): return tf.raw_ops.MatrixSetDiag(input=input, diagonal=diagonal) @I.ir_module class Expected: @R.function def main( input: R.Tensor((3, 3), dtype="float32"), diagonal: R.Tensor((3,), dtype="float32"), ) -> R.Tensor((3, 3), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv = R.call_dps_packed( "topi.matrix_set_diag", ( input, diagonal, R.const(0, "int32"), R.const(0, "int32"), R.const(False, "bool"), R.const(False, "bool"), ), out_ty=R.Tensor((3, 3), dtype="float32"), ) R.output(gv) return gv verify(MatrixSetDiag, Expected) def test_sparse_to_dense(): """Test TFLite SPARSE_TO_DENSE operator.""" class SparseToDense(tf.Module): @tf.function( input_signature=[ tf.TensorSpec(shape=(2,), dtype=tf.int32), tf.TensorSpec(shape=(2,), dtype=tf.float32), tf.TensorSpec(shape=(), dtype=tf.float32), ] ) def func(self, indices, values, default_value): # output_shape is provided as a constant, not an input return tf.raw_ops.SparseToDense( sparse_indices=indices, output_shape=tf.constant([3], dtype=tf.int32), sparse_values=values, default_value=default_value, ) @I.ir_module class Expected: @R.function def main( indices: R.Tensor((2,), dtype="int32"), values: R.Tensor((2,), dtype="float32"), default_value: R.Tensor((), dtype="float32"), ) -> R.Tensor((3,), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv = R.call_dps_packed( "topi.sparse_to_dense", (indices, R.const([3], "int32"), values, default_value), out_ty=R.Tensor((3,), dtype="float32"), ) R.output(gv) return gv verify(SparseToDense, Expected) # DENSIFY operator tests # DENSIFY converts sparse weight tensors to dense at conversion time (not runtime). # Since TensorFlow does not provide an API to create sparse TFLite models, # we manually build them using the flatbuffers API. # Import schema helpers explicitly. CI's generated tflite package does not # reliably re-export these builder helpers and enums at the package top-level. def _get_tflite_schema_module(module_name): return __import__(f"tflite.{module_name}", fromlist=[module_name]) def _get_tflite_schema_enum(enum_name): return getattr(_get_tflite_schema_module(enum_name), enum_name) _tfl_add_options = _get_tflite_schema_module("AddOptions") _tfl_buffer = _get_tflite_schema_module("Buffer") _tfl_concatenation_options = _get_tflite_schema_module("ConcatenationOptions") _tfl_conv2d_options = _get_tflite_schema_module("Conv2DOptions") _tfl_depthwise_conv2d_options = _get_tflite_schema_module("DepthwiseConv2DOptions") _tfl_dilate_options = _get_tflite_schema_module("DilateOptions") _tfl_reshape_options = _get_tflite_schema_module("ReshapeOptions") _tfl_transpose_conv_options = _get_tflite_schema_module("TransposeConvOptions") # ── StableHLO BuiltinOptions2 schema modules ──────────────────────────── _tfl_stablehlo_concat_opts = _get_tflite_schema_module("StablehloConcatenateOptions") _tfl_stablehlo_bcast_opts = _get_tflite_schema_module("StablehloBroadcastInDimOptions") _tfl_stablehlo_composite_opts = _get_tflite_schema_module("StableHLOCompositeOptions") _tfl_stablehlo_conv_opts = _get_tflite_schema_module("StablehloConvolutionOptions") _tfl_stablehlo_custom_call_opts = _get_tflite_schema_module("StablehloCustomCallOptions") _tfl_stablehlo_dot_opts = _get_tflite_schema_module("StablehloDotGeneralOptions") _tfl_stablehlo_iota_opts = _get_tflite_schema_module("StablehloIotaOptions") _tfl_stablehlo_compare_opts = _get_tflite_schema_module("StablehloCompareOptions") _tfl_stablehlo_comp_dir = _get_tflite_schema_module("StablehloComparisonDirection") _tfl_stablehlo_comp_type = _get_tflite_schema_module("StablehloComparisonType") _tfl_stablehlo_pad_opts = _get_tflite_schema_module("StablehloPadOptions") _tfl_stablehlo_dyn_slice_opts = _get_tflite_schema_module("StablehloDynamicSliceOptions") _tfl_stablehlo_gather_opts = _get_tflite_schema_module("StablehloGatherOptions") _tfl_stablehlo_reduce_opts = _get_tflite_schema_module("StablehloReduceOptions") _tfl_stablehlo_reduce_window_opts = _get_tflite_schema_module("StablehloReduceWindowOptions") _tfl_stablehlo_scatter_opts = _get_tflite_schema_module("StablehloScatterOptions") _tfl_stablehlo_sort_opts = _get_tflite_schema_module("StablehloSortOptions") _tfl_stablehlo_while_opts = _get_tflite_schema_module("StablehloWhileOptions") _tfl_stablehlo_rng_opts = _get_tflite_schema_module("StablehloRngBitGeneratorOptions") _tfl_call_options = _get_tflite_schema_module("CallOptions") _tfl_call_once_options = _get_tflite_schema_module("CallOnceOptions") _tfl_dimension_metadata = _get_tflite_schema_module("DimensionMetadata") _tfl_fully_connected_options = _get_tflite_schema_module("FullyConnectedOptions") _tfl_if_options = _get_tflite_schema_module("IfOptions") _tfl_int32_vector = _get_tflite_schema_module("Int32Vector") _tfl_model = _get_tflite_schema_module("Model") _tfl_operator = _get_tflite_schema_module("Operator") _tfl_operator_code = _get_tflite_schema_module("OperatorCode") _tfl_pool2d_options = _get_tflite_schema_module("Pool2DOptions") _tfl_quantization_parameters = _get_tflite_schema_module("QuantizationParameters") _tfl_sparsity_parameters = _get_tflite_schema_module("SparsityParameters") _tfl_subgraph = _get_tflite_schema_module("SubGraph") _tfl_tensor = _get_tflite_schema_module("Tensor") _tfl_reverse_sequence_options = _get_tflite_schema_module("ReverseSequenceOptions") _tfl_squeeze_options = _get_tflite_schema_module("SqueezeOptions") _tfl_unpack_options = _get_tflite_schema_module("UnpackOptions") _tfl_while_options = _get_tflite_schema_module("WhileOptions") _tfl_zeros_like_options = _get_tflite_schema_module("ZerosLikeOptions") _tfl_builtin_operator = _get_tflite_schema_enum("BuiltinOperator") _tfl_builtin_options = _get_tflite_schema_enum("BuiltinOptions") _tfl_builtin_options2 = _get_tflite_schema_enum("BuiltinOptions2") _tfl_activation_fn = _get_tflite_schema_enum("ActivationFunctionType") _tfl_dimension_type = _get_tflite_schema_enum("DimensionType") _tfl_fc_weights_format = _get_tflite_schema_enum("FullyConnectedOptionsWeightsFormat") _tfl_padding = _get_tflite_schema_enum("Padding") _tfl_sparse_index_vector = _get_tflite_schema_enum("SparseIndexVector") _tfl_tensor_type = _get_tflite_schema_enum("TensorType") _tfl_rng_algorithm = _get_tflite_schema_enum("RngAlgorithm") _tfl_lstm_options = _get_tflite_schema_module("LSTMOptions") _tfl_sequence_rnn_options = _get_tflite_schema_module("SequenceRNNOptions") _tfl_svdf_options = _get_tflite_schema_module("SVDFOptions") _tfl_unidirectional_sequence_lstm_options = _get_tflite_schema_module( "UnidirectionalSequenceLSTMOptions" ) _tfl_bidirectional_sequence_rnn_options = _get_tflite_schema_module( "BidirectionalSequenceRNNOptions" ) _tfl_bidirectional_sequence_lstm_options = _get_tflite_schema_module( "BidirectionalSequenceLSTMOptions" ) _DENSIFY_TEST_VALUES = np.array([1.0, 2.0], dtype=np.float32) _DENSIFY_TEST_DENSE = np.array([[1.0, 0.0], [0.0, 2.0]], dtype=np.float32) _DENSIFY_ROW_PTRS = [0, 1, 2] _DENSIFY_COL_INDICES = [0, 1] _DENSIFY_CONV_KERNEL_DENSE_HWIO = _DENSIFY_TEST_DENSE.reshape(2, 2, 1, 1) _DENSIFY_FC_WEIGHT_VALUES = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32) _DENSIFY_FC_WEIGHT_DENSE_OI = np.diag(_DENSIFY_FC_WEIGHT_VALUES).astype(np.float32) _DENSIFY_FC_ROW_PTRS = [0, 1, 2, 3, 4] _DENSIFY_FC_COL_INDICES = [0, 1, 2, 3] def _tflite_int32_vector(builder, start_vector_fn, values): start_vector_fn(builder, len(values)) for value in reversed(values): builder.PrependInt32(value) return builder.EndVector() def _tflite_int64_vector(builder, start_vector_fn, values): start_vector_fn(builder, len(values)) for value in reversed(values): builder.PrependInt64(value) return builder.EndVector() def _tflite_bool_vector(builder, start_vector_fn, values): start_vector_fn(builder, len(values)) for value in reversed(values): builder.PrependBool(value) return builder.EndVector() def _tflite_float32_vector(builder, start_vector_fn, values): start_vector_fn(builder, len(values)) for value in reversed(values): builder.PrependFloat32(value) return builder.EndVector() def _tflite_offset_vector(builder, start_vector_fn, offsets): start_vector_fn(builder, len(offsets)) for offset in reversed(offsets): builder.PrependUOffsetTRelative(offset) return builder.EndVector() def _tflite_byte_vector(builder, data): _tfl_buffer.BufferStartDataVector(builder, len(data)) for byte in reversed(data): builder.PrependByte(byte) return builder.EndVector() def _tflite_int32_table(builder, values): # Build the values vector directly without relying on version-specific # helper Int32VectorStartValuesVector, which is absent in older # tflite package versions used in CI. builder.StartVector(4, len(values), 4) for value in reversed(values): builder.PrependInt32(value) values_vec = builder.EndVector() _tfl_int32_vector.Int32VectorStart(builder) _tfl_int32_vector.Int32VectorAddValues(builder, values_vec) return _tfl_int32_vector.Int32VectorEnd(builder) def _tflite_shape(builder, shape): return _tflite_int32_vector(builder, _tfl_tensor.TensorStartShapeVector, shape) def _build_tensor(builder, buffer_idx, shape, sparsity=None, tensor_type=None, quantization=None): """Helper to build a TFLite tensor.""" if tensor_type is None: tensor_type = _tfl_tensor_type.FLOAT32 shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buffer_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, False) _tfl_tensor.TensorAddShape(builder, shape_vec) if sparsity is not None: _tfl_tensor.TensorAddSparsity(builder, sparsity) if quantization is not None: _tfl_tensor.TensorAddQuantization(builder, quantization) _tfl_tensor.TensorAddType(builder, tensor_type) return _tfl_tensor.TensorEnd(builder) def _build_buffer(builder, data=None): # Build the data vector before starting the Buffer table to avoid # flatbuffers IsNestedError (vectors cannot be created inside tables). data_offset = None if data is not None: data_offset = _tflite_byte_vector(builder, data) _tfl_buffer.BufferStart(builder) if data_offset is not None: _tfl_buffer.BufferAddData(builder, data_offset) return _tfl_buffer.BufferEnd(builder) def _build_quantization_parameters(builder, *, scale, zero_point, quantized_dimension): scale_vec = _tflite_float32_vector( builder, _tfl_quantization_parameters.QuantizationParametersStartScaleVector, scale ) zero_point_vec = _tflite_int64_vector( builder, _tfl_quantization_parameters.QuantizationParametersStartZeroPointVector, zero_point, ) _tfl_quantization_parameters.QuantizationParametersStart(builder) _tfl_quantization_parameters.QuantizationParametersAddScale(builder, scale_vec) _tfl_quantization_parameters.QuantizationParametersAddZeroPoint(builder, zero_point_vec) _tfl_quantization_parameters.QuantizationParametersAddQuantizedDimension( builder, quantized_dimension ) return _tfl_quantization_parameters.QuantizationParametersEnd(builder) def _build_operator( builder, opcode_index, inputs, outputs, builtin_options_type=None, builtin_options=None, builtin_options2_type=None, builtin_options2=None, ): inputs_vec = _tflite_int32_vector(builder, _tfl_operator.OperatorStartInputsVector, inputs) outputs_vec = _tflite_int32_vector(builder, _tfl_operator.OperatorStartOutputsVector, outputs) _tfl_operator.OperatorStart(builder) _tfl_operator.OperatorAddOpcodeIndex(builder, opcode_index) _tfl_operator.OperatorAddInputs(builder, inputs_vec) _tfl_operator.OperatorAddOutputs(builder, outputs_vec) if builtin_options_type is not None: _tfl_operator.OperatorAddBuiltinOptionsType(builder, builtin_options_type) if builtin_options is not None: _tfl_operator.OperatorAddBuiltinOptions(builder, builtin_options) if builtin_options2_type is not None: _tfl_operator.OperatorAddBuiltinOptions2Type(builder, builtin_options2_type) if builtin_options2 is not None: _tfl_operator.OperatorAddBuiltinOptions2(builder, builtin_options2) return _tfl_operator.OperatorEnd(builder) def _build_operator_code(builder, builtin_op): # deprecated_builtin_code is int8 (max 127). Ops past that write 127 as a # placeholder and use the full builtin_code field. deprecated_code = builtin_op if builtin_op < 127 else 127 _tfl_operator_code.OperatorCodeStart(builder) _tfl_operator_code.OperatorCodeAddDeprecatedBuiltinCode(builder, deprecated_code) _tfl_operator_code.OperatorCodeAddBuiltinCode(builder, builtin_op) _tfl_operator_code.OperatorCodeAddVersion(builder, 1) return _tfl_operator_code.OperatorCodeEnd(builder) def _build_subgraph(builder, *, tensors, operators, inputs, outputs): tensors_vec = _tflite_offset_vector(builder, _tfl_subgraph.SubGraphStartTensorsVector, tensors) operators_vec = _tflite_offset_vector( builder, _tfl_subgraph.SubGraphStartOperatorsVector, operators ) inputs_vec = _tflite_int32_vector(builder, _tfl_subgraph.SubGraphStartInputsVector, inputs) outputs_vec = _tflite_int32_vector(builder, _tfl_subgraph.SubGraphStartOutputsVector, outputs) _tfl_subgraph.SubGraphStart(builder) _tfl_subgraph.SubGraphAddTensors(builder, tensors_vec) _tfl_subgraph.SubGraphAddOperators(builder, operators_vec) _tfl_subgraph.SubGraphAddInputs(builder, inputs_vec) _tfl_subgraph.SubGraphAddOutputs(builder, outputs_vec) return _tfl_subgraph.SubGraphEnd(builder) def _finish_tflite_model(builder, *, subgraph, operator_codes, buffers, extra_subgraphs=None): all_subgraphs = [subgraph] + (extra_subgraphs or []) buffers_vec = _tflite_offset_vector(builder, _tfl_model.ModelStartBuffersVector, buffers) opcodes_vec = _tflite_offset_vector( builder, _tfl_model.ModelStartOperatorCodesVector, operator_codes ) subgraphs_vec = _tflite_offset_vector( builder, _tfl_model.ModelStartSubgraphsVector, all_subgraphs ) _tfl_model.ModelStart(builder) _tfl_model.ModelAddBuffers(builder, buffers_vec) _tfl_model.ModelAddSubgraphs(builder, subgraphs_vec) _tfl_model.ModelAddOperatorCodes(builder, opcodes_vec) _tfl_model.ModelAddVersion(builder, 3) model = _tfl_model.ModelEnd(builder) builder.Finish(model, b"TFL3") return bytes(builder.Output()) def _build_call_options(builder, subgraph_index): _tfl_call_options.CallOptionsStart(builder) _tfl_call_options.CallOptionsAddSubgraph(builder, subgraph_index) return _tfl_call_options.CallOptionsEnd(builder) def _build_if_options(builder, then_subgraph_index, else_subgraph_index): _tfl_if_options.IfOptionsStart(builder) _tfl_if_options.IfOptionsAddThenSubgraphIndex(builder, then_subgraph_index) _tfl_if_options.IfOptionsAddElseSubgraphIndex(builder, else_subgraph_index) return _tfl_if_options.IfOptionsEnd(builder) def _build_while_options(builder, cond_subgraph_index, body_subgraph_index): _tfl_while_options.WhileOptionsStart(builder) _tfl_while_options.WhileOptionsAddCondSubgraphIndex(builder, cond_subgraph_index) _tfl_while_options.WhileOptionsAddBodySubgraphIndex(builder, body_subgraph_index) return _tfl_while_options.WhileOptionsEnd(builder) def _build_stablehlo_while_options(builder, cond_subgraph_index, body_subgraph_index): _tfl_stablehlo_while_opts.StablehloWhileOptionsStart(builder) _tfl_stablehlo_while_opts.StablehloWhileOptionsAddCondSubgraphIndex( builder, cond_subgraph_index ) _tfl_stablehlo_while_opts.StablehloWhileOptionsAddBodySubgraphIndex( builder, body_subgraph_index ) return _tfl_stablehlo_while_opts.StablehloWhileOptionsEnd(builder) def _build_call_once_options(builder, init_subgraph_index): _tfl_call_once_options.CallOnceOptionsStart(builder) _tfl_call_once_options.CallOnceOptionsAddInitSubgraphIndex(builder, init_subgraph_index) return _tfl_call_once_options.CallOnceOptionsEnd(builder) def _build_squeeze_options(builder, squeeze_dims): squeeze_dims_vec = _tflite_int32_vector( builder, _tfl_squeeze_options.SqueezeOptionsStartSqueezeDimsVector, squeeze_dims, ) _tfl_squeeze_options.SqueezeOptionsStart(builder) _tfl_squeeze_options.SqueezeOptionsAddSqueezeDims(builder, squeeze_dims_vec) return _tfl_squeeze_options.SqueezeOptionsEnd(builder) def _build_reverse_sequence_options(builder, seq_dim, batch_dim): _tfl_reverse_sequence_options.ReverseSequenceOptionsStart(builder) _tfl_reverse_sequence_options.ReverseSequenceOptionsAddSeqDim(builder, seq_dim) _tfl_reverse_sequence_options.ReverseSequenceOptionsAddBatchDim(builder, batch_dim) return _tfl_reverse_sequence_options.ReverseSequenceOptionsEnd(builder) def _build_unpack_options(builder, num, axis): _tfl_unpack_options.UnpackOptionsStart(builder) _tfl_unpack_options.UnpackOptionsAddNum(builder, num) _tfl_unpack_options.UnpackOptionsAddAxis(builder, axis) return _tfl_unpack_options.UnpackOptionsEnd(builder) def _get_builtin_options_type(options_name): if not hasattr(_tfl_builtin_options, options_name): pytest.skip(f"TFLite schema does not provide BuiltinOptions.{options_name}") return getattr(_tfl_builtin_options, options_name) def _get_resource_tensor_type(): if not hasattr(_tfl_tensor_type, "RESOURCE"): pytest.skip("TFLite schema does not provide TensorType.RESOURCE") return getattr(_tfl_tensor_type, "RESOURCE") def _get_string_tensor_type(): if not hasattr(_tfl_tensor_type, "STRING"): pytest.skip("TFLite schema does not provide TensorType.STRING") return getattr(_tfl_tensor_type, "STRING") def _build_tflite_string_buffer(values): encoded = [value.encode("utf-8") for value in values] offsets = [] cursor = 4 * (len(encoded) + 2) for value in encoded: offsets.append(cursor) cursor += len(value) offsets.append(cursor) header = np.array([len(encoded), *offsets], dtype=np.int32).tobytes() return header + b"".join(encoded) def _build_var_handle_options(builder, shared_name="resource_var", container=""): try: var_handle_options = _get_tflite_schema_module("VarHandleOptions") except ModuleNotFoundError: pytest.skip("TFLite schema does not provide VarHandleOptions") container_offset = builder.CreateString(container) shared_name_offset = builder.CreateString(shared_name) var_handle_options.VarHandleOptionsStart(builder) var_handle_options.VarHandleOptionsAddContainer(builder, container_offset) var_handle_options.VarHandleOptionsAddSharedName(builder, shared_name_offset) return var_handle_options.VarHandleOptionsEnd(builder) def _build_empty_builtin_options(builder, options_name): try: options_module = _get_tflite_schema_module(options_name) except ModuleNotFoundError: pytest.skip(f"TFLite schema does not provide {options_name}") getattr(options_module, f"{options_name}Start")(builder) return getattr(options_module, f"{options_name}End")(builder) def _build_hashtable_options( builder, table_id=0, key_dtype=None, value_dtype=None, ): try: hashtable_options = _get_tflite_schema_module("HashtableOptions") except ModuleNotFoundError: pytest.skip("TFLite schema does not provide HashtableOptions") key_dtype = _tfl_tensor_type.INT64 if key_dtype is None else key_dtype value_dtype = _get_string_tensor_type() if value_dtype is None else value_dtype hashtable_options.HashtableOptionsStart(builder) hashtable_options.HashtableOptionsAddTableId(builder, table_id) hashtable_options.HashtableOptionsAddKeyDtype(builder, key_dtype) hashtable_options.HashtableOptionsAddValueDtype(builder, value_dtype) return hashtable_options.HashtableOptionsEnd(builder) def _build_embedding_lookup_sparse_options(builder, combiner): try: sparse_options = _get_tflite_schema_module("EmbeddingLookupSparseOptions") except ModuleNotFoundError: pytest.skip("TFLite schema does not provide EmbeddingLookupSparseOptions") sparse_options.EmbeddingLookupSparseOptionsStart(builder) sparse_options.EmbeddingLookupSparseOptionsAddCombiner(builder, combiner) return sparse_options.EmbeddingLookupSparseOptionsEnd(builder) def _load_model_from_buffer(model_bytes): if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(model_bytes, 0) else: tflite_model = tflite.Model.GetRootAsModel(model_bytes, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") return mod def _get_builtin_operator(builtin_name): if not hasattr(_tfl_builtin_operator, builtin_name): pytest.skip(f"TFLite schema does not provide BuiltinOperator.{builtin_name}") return getattr(_tfl_builtin_operator, builtin_name) def _build_tflite_operator_marker_model(builtin_name): """Build a minimal model containing a TFLite marker builtin.""" builder = flatbuffers.Builder(1024) builtin_op = _get_builtin_operator(builtin_name) op_code = _build_operator_code(builder, builtin_op) tensors = [ _build_tensor(builder, 0, [1], tensor_type=_tfl_tensor_type.FLOAT32), _build_tensor(builder, 0, [1], tensor_type=_tfl_tensor_type.FLOAT32), ] op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0], outputs=[1]) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=[_build_buffer(builder)], ) @pytest.mark.parametrize("builtin_name", ["DELEGATE", "PLACEHOLDER_FOR_GREATER_OP_CODES"]) def test_operator_marker_unsupported(builtin_name): """TFLite marker builtins report explicit unsupported diagnostics.""" with pytest.raises(tvm.error.OpNotImplemented, match=f"TFLite operator marker {builtin_name}"): _load_model_from_buffer(_build_tflite_operator_marker_model(builtin_name)) def _build_tflite_squeeze_model(): builder = flatbuffers.Builder(1024) squeeze_opts = _build_squeeze_options(builder, [0, 2]) squeeze_op_code = _build_operator_code(builder, _tfl_builtin_operator.SQUEEZE) tensors = [ _build_tensor(builder, 0, [1, 2, 1, 3]), _build_tensor(builder, 0, [2, 3]), ] squeeze_op = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.SqueezeOptions, builtin_options=squeeze_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[squeeze_op], inputs=[0], outputs=[1], ) buffers = [_build_buffer(builder)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[squeeze_op_code], buffers=buffers, ) def _build_tflite_reverse_sequence_model(): builder = flatbuffers.Builder(1024) reverse_sequence_opts = _build_reverse_sequence_options(builder, seq_dim=1, batch_dim=0) reverse_sequence_op_code = _build_operator_code(builder, _tfl_builtin_operator.REVERSE_SEQUENCE) tensors = [ _build_tensor(builder, 0, [2, 4, 3]), _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 0, [2, 4, 3]), ] reverse_sequence_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.ReverseSequenceOptions, builtin_options=reverse_sequence_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[reverse_sequence_op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[reverse_sequence_op_code], buffers=buffers, ) def _build_tflite_unpack_model(): builder = flatbuffers.Builder(1024) unpack_opts = _build_unpack_options(builder, num=3, axis=1) unpack_op_code = _build_operator_code(builder, _tfl_builtin_operator.UNPACK) tensors = [ _build_tensor(builder, 0, [2, 3, 4]), _build_tensor(builder, 0, [2, 4]), _build_tensor(builder, 0, [2, 4]), _build_tensor(builder, 0, [2, 4]), ] unpack_op = _build_operator( builder, 0, [0], [1, 2, 3], builtin_options_type=_tfl_builtin_options.UnpackOptions, builtin_options=unpack_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[unpack_op], inputs=[0], outputs=[1, 2, 3], ) buffers = [_build_buffer(builder)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[unpack_op_code], buffers=buffers, ) def _build_tflite_zeros_like_model(): builder = flatbuffers.Builder(1024) _tfl_zeros_like_options.ZerosLikeOptionsStart(builder) zeros_like_opts = _tfl_zeros_like_options.ZerosLikeOptionsEnd(builder) zeros_like_op_code = _build_operator_code(builder, _tfl_builtin_operator.ZEROS_LIKE) tensors = [ _build_tensor(builder, 0, [2, 3]), _build_tensor(builder, 0, [2, 3]), ] zeros_like_op = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.ZerosLikeOptions, builtin_options=zeros_like_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[zeros_like_op], inputs=[0], outputs=[1], ) buffers = [_build_buffer(builder)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[zeros_like_op_code], buffers=buffers, ) def _run_module(mod, *inputs): tgt = tvm.target.Target("c") ex = tvm.compile(mod, tgt) vm = relax.VirtualMachine(ex, tvm.cpu()) vm.set_input("main", *inputs) vm.invoke_stateful("main") outputs = vm.get_outputs("main") if hasattr(outputs, "numpy"): return outputs.numpy() return tuple(output.numpy() for output in outputs) def _run_no_input_module(mod): return _run_module(mod) def _complex64_to_pair(value): value = np.asarray(value, dtype=np.complex64) return np.stack([value.real, value.imag], axis=-1).astype("float32") def _build_tflite_rfft2d_model(*, input_shape, fft_length, output_shape): """Build a minimal TFLite RFFT2D model.""" builder = flatbuffers.Builder(1024) builtin_op = _get_builtin_operator("RFFT2D") op_code = _build_operator_code(builder, builtin_op) tensors = [ _build_tensor(builder, 0, input_shape, tensor_type=_tfl_tensor_type.FLOAT32), _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 2, output_shape, tensor_type=_tfl_tensor_type.COMPLEX64), ] op = _build_operator(builder, 0, [0, 1], [2]) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0], outputs=[2]) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array(fft_length, dtype=np.int32).tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_rfft2d_static_pair_output(): """TFLite RFFT2D emits a call_tir kernel with float32 real/imag pair output.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[2, 4], fft_length=[2, 4], output_shape=[2, 3], ) ) data = np.array([[1.0, -2.0, 3.0, 4.0], [5.0, 6.0, -7.0, 8.0]], dtype="float32") expected = np.fft.rfft2(data).astype(np.complex64) # atol accommodates the float32 reference kernel: numpy's rfft2 internally uses # float64, while the reference TIR kernel accumulates in float32 (see # _build_tflite_rfft2d_primfunc docstring). np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-5, atol=1e-5 ) def test_rfft2d_static_pair_output_with_batch(): """RFFT2D computes over the last two axes and preserves leading batch dimensions.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[2, 2, 4], fft_length=[2, 4], output_shape=[2, 2, 3], ) ) data = np.array( [ [[1.0, -2.0, 3.0, 4.0], [5.0, 6.0, -7.0, 8.0]], [[-1.0, 2.0, 0.5, -4.0], [3.5, -6.0, 7.0, 1.0]], ], dtype="float32", ) expected = np.fft.rfft2(data).astype(np.complex64) np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-5, atol=1e-5 ) def test_rfft2d_odd_width_pair_output(): """RFFT2D handles odd width: output has width//2 + 1 bins (TFLite convention).""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[3, 5], fft_length=[3, 5], output_shape=[3, 3], # 5 // 2 + 1 = 3 ) ) data = np.array( [[1.0, -2.0, 3.0, 4.0, -5.0], [0.5, 6.0, -7.0, 8.0, 2.5], [-1.5, 4.0, 0.0, -3.0, 1.0]], dtype="float32", ) expected = np.fft.rfft2(data).astype(np.complex64) # atol accommodates the float32 reference kernel (see # _build_tflite_rfft2d_primfunc docstring). np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-5, atol=1e-5 ) def test_rfft2d_int64_fft_length(): """RFFT2D accepts INT64 fft_length constant (TFLite schema allows either int32 or int64).""" builder = flatbuffers.Builder(1024) rfft_op_code = _build_operator_code(builder, _get_builtin_operator("RFFT2D")) tensors = [ _build_tensor(builder, 0, [2, 4], tensor_type=_tfl_tensor_type.FLOAT32), _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT64), _build_tensor(builder, 2, [2, 3], tensor_type=_tfl_tensor_type.COMPLEX64), ] op = _build_operator(builder, 0, [0, 1], [2]) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0], outputs=[2]) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array([2, 4], dtype=np.int64).tobytes()), _build_buffer(builder), ] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[rfft_op_code], buffers=buffers ) mod = _load_model_from_buffer(buf) data = np.array([[1.0, -2.0, 3.0, 4.0], [5.0, 6.0, -7.0, 8.0]], dtype="float32") expected = np.fft.rfft2(data).astype(np.complex64) np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-5, atol=1e-5 ) def test_rfft2d_4d_input_pair_output(): """RFFT2D accepts 4D input and preserves leading batch dimensions.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[2, 3, 4, 5], # batch=6, H=4, W=5 fft_length=[4, 5], output_shape=[2, 3, 4, 3], # 5 // 2 + 1 = 3 ) ) rng = np.random.RandomState(0) data = (rng.randn(2, 3, 4, 5) * 0.5).astype("float32") expected = np.fft.rfft2(data).astype(np.complex64) # 4D test accumulates 20 inner terms per output; use a slightly larger atol # than the 2D case (which accumulates 4-8 terms). np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-5, atol=1e-4 ) def test_rfft2d_minimal_1x1_pair_output(): """RFFT2D on a [1, 1] input: the only output is the DC component (sum of inputs).""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[1, 1], fft_length=[1, 1], output_shape=[1, 1], ) ) data = np.array([[3.5]], dtype="float32") expected = np.fft.rfft2(data).astype(np.complex64) np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-5, atol=1e-5 ) def test_rfft2d_fft_path_8x8(): """RFFT2D on a square 8x8 input exercises the Cooley-Tukey FFT dispatch path.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[8, 8], fft_length=[8, 8], output_shape=[8, 5], ) ) np.random.seed(0xCAFE) data = np.random.randn(8, 8).astype("float32") expected = np.fft.rfft2(data).astype(np.complex64) # The FFT path uses float32 twiddles (cos/sin) and float32 butterfly # accumulation, so the error vs. numpy's float64 reference is in the # 1e-4 range on these random inputs. np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-4, atol=1e-4 ) def test_rfft2d_fft_path_4x4(): """RFFT2D on a 4x4 input: smallest case where both row and column FFTs do real work.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[4, 4], fft_length=[4, 4], output_shape=[4, 3], ) ) np.random.seed(0xFEED) data = np.random.randn(4, 4).astype("float32") expected = np.fft.rfft2(data).astype(np.complex64) np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-4, atol=1e-4 ) def test_rfft2d_fft_path_2x2x4x8(): """RFFT2D on a 4D input with power-of-2 height/width exercises the FFT path with batch.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[2, 2, 4, 8], fft_length=[4, 8], output_shape=[2, 2, 4, 5], ) ) np.random.seed(0xBEEF) data = np.random.randn(2, 2, 4, 8).astype("float32") expected = np.fft.rfft2(data, axes=(-2, -1)).astype(np.complex64) np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-4, atol=1e-4 ) def test_rfft2d_fft_path_16x16(): """RFFT2D on a 16x16 input: a larger FFT to check that the unrolled kernel scales.""" mod = _load_model_from_buffer( _build_tflite_rfft2d_model( input_shape=[16, 16], fft_length=[16, 16], output_shape=[16, 9], ) ) np.random.seed(0xDEAD) data = np.random.randn(16, 16).astype("float32") expected = np.fft.rfft2(data).astype(np.complex64) np.testing.assert_allclose( _run_module(mod, data), _complex64_to_pair(expected), rtol=1e-4, atol=1e-4 ) def test_rfft2d_mismatched_fft_length_unsupported(): """RFFT2D padding/truncation cases are guarded until explicitly implemented.""" buf = _build_tflite_rfft2d_model( input_shape=[2, 4], fft_length=[4, 4], output_shape=[4, 3], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="fft_length"): from_tflite(tflite_model) def test_rfft2d_dynamic_fft_length_unsupported(): """RFFT2D requires fft_length to be a constant tensor.""" builder = flatbuffers.Builder(1024) rfft_op_code = _build_operator_code(builder, _get_builtin_operator("RFFT2D")) tensors = [ _build_tensor(builder, 0, [2, 4], tensor_type=_tfl_tensor_type.FLOAT32), _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 2, [2, 3], tensor_type=_tfl_tensor_type.COMPLEX64), ] op = _build_operator(builder, 0, [0, 1], [2]) subgraph = _build_subgraph(builder, tensors=tensors, operators=[op], inputs=[0, 1], outputs=[2]) buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[rfft_op_code], buffers=[_build_buffer(builder), _build_buffer(builder), _build_buffer(builder)], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="requires a constant fft_length"): from_tflite(tflite_model) def _build_tflite_call_model( call_subgraph_index=1, callee_inputs=None, callee_outputs=None, callee_output_shape=None, callee_output_type=None, ): """Build a TFLite model where main CALLs a subgraph computing x + 1.""" builder = flatbuffers.Builder(1024) callee_inputs = [0] if callee_inputs is None else callee_inputs callee_outputs = [2] if callee_outputs is None else callee_outputs callee_output_shape = [2, 2] if callee_output_shape is None else callee_output_shape callee_output_type = ( _tfl_tensor_type.FLOAT32 if callee_output_type is None else callee_output_type ) call_options = _build_call_options(builder, call_subgraph_index) one = np.array(1.0, dtype=np.float32) main_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 2, [2, 2]), ] main_call = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.CallOptions, builtin_options=call_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_call], inputs=[0], outputs=[1], ) callee_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, []), _build_tensor(builder, 2, callee_output_shape, tensor_type=callee_output_type), ] callee_add = _build_operator(builder, 1, [0, 1], [2]) callee_subgraph = _build_subgraph( builder, tensors=callee_tensors, operators=[callee_add], inputs=callee_inputs, outputs=callee_outputs, ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL")), _build_operator_code(builder, _get_builtin_operator("ADD")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, one.tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[callee_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_call_subgraph(): """Test TFLite CALL conversion to a private Relax function.""" mod = _load_model_from_buffer(_build_tflite_call_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_call_subgraph_1( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add( tvmgen_tensor_0, R.const(1.0, "float32") ) R.output(gv) return gv @R.function def main( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) cls = Expected with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = cls.tflite_call_subgraph_1(tvmgen_tensor_0) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_tflite_multi_output_call_model(): """Build a TFLite model where CALL returns x + 1 and x - 1.""" builder = flatbuffers.Builder(1024) call_options = _build_call_options(builder, 1) one = np.array(1.0, dtype=np.float32) main_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 2, [2, 2]), _build_tensor(builder, 3, [2, 2]), ] main_call = _build_operator( builder, 0, [0], [1, 2], builtin_options_type=_tfl_builtin_options.CallOptions, builtin_options=call_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_call], inputs=[0], outputs=[1, 2], ) callee_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, []), _build_tensor(builder, 2, [2, 2]), _build_tensor(builder, 3, [2, 2]), ] callee_add = _build_operator(builder, 1, [0, 1], [2]) callee_sub = _build_operator(builder, 2, [0, 1], [3]) callee_subgraph = _build_subgraph( builder, tensors=callee_tensors, operators=[callee_add, callee_sub], inputs=[0], outputs=[2, 3], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL")), _build_operator_code(builder, _get_builtin_operator("ADD")), _build_operator_code(builder, _get_builtin_operator("SUB")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, one.tobytes()), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[callee_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_call_subgraph_multi_output(): """Test CALL tuple returns are split and rebound to TFLite output tensors.""" mod = _load_model_from_buffer(_build_tflite_multi_output_call_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_call_subgraph_1( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add( tvmgen_tensor_0, R.const(1.0, "float32") ) gv1: R.Tensor((2, 2), dtype="float32") = R.subtract( tvmgen_tensor_0, R.const(1.0, "float32") ) gv2: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = (gv, gv1) R.output(gv2) return gv2 @R.function def main( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): R.func_attr({"num_input": 1}) cls = Expected with R.dataflow(): lv: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = cls.tflite_call_subgraph_1(tvmgen_tensor_0) lv1: R.Tensor((2, 2), dtype="float32") = lv[0] lv2: R.Tensor((2, 2), dtype="float32") = lv[1] gv: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = (lv1, lv2) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_tflite_nested_call_model(): """Build a TFLite model where main CALLs subgraph A, which CALLs subgraph B.""" builder = flatbuffers.Builder(1024) main_call_options = _build_call_options(builder, 1) nested_call_options = _build_call_options(builder, 2) one = np.array(1.0, dtype=np.float32) main_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 3, [2, 2]), ] main_call = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.CallOptions, builtin_options=main_call_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_call], inputs=[0], outputs=[1], ) caller_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 3, [2, 2]), ] nested_call = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.CallOptions, builtin_options=nested_call_options, ) caller_subgraph = _build_subgraph( builder, tensors=caller_tensors, operators=[nested_call], inputs=[0], outputs=[1], ) callee_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, []), _build_tensor(builder, 3, [2, 2]), ] callee_add = _build_operator(builder, 1, [0, 1], [2]) callee_subgraph = _build_subgraph( builder, tensors=callee_tensors, operators=[callee_add], inputs=[0], outputs=[2], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL")), _build_operator_code(builder, _get_builtin_operator("ADD")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, one.tobytes()), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[caller_subgraph, callee_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_call_subgraph_nested_call(): """Test nested CALL subgraphs register all generated private functions.""" mod = _load_model_from_buffer(_build_tflite_nested_call_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_call_subgraph_2( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add( tvmgen_tensor_0, R.const(1.0, "float32") ) R.output(gv) return gv @R.function(private=True) def tflite_call_subgraph_1( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): cls = Expected with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = cls.tflite_call_subgraph_2(tvmgen_tensor_0) R.output(gv) return gv @R.function def main( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) cls = Expected with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = cls.tflite_call_subgraph_1(tvmgen_tensor_0) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_call_subgraph_invalid_index_unsupported(): """Test CALL rejects invalid subgraph indices before lowering.""" with pytest.raises(tvm.error.OpNotImplemented, match="CALL requires a valid subgraph index"): _load_model_from_buffer(_build_tflite_call_model(call_subgraph_index=2)) def test_call_subgraph_io_mismatch_unsupported(): """Test CALL rejects callees whose input arity does not match the call site.""" with pytest.raises(tvm.error.OpNotImplemented, match="CALL subgraph input count mismatch"): _load_model_from_buffer(_build_tflite_call_model(callee_inputs=[])) def test_call_subgraph_output_metadata_mismatch_unsupported(): """Test CALL rejects callees whose output metadata does not match the call site.""" with pytest.raises( tvm.error.OpNotImplemented, match="CALL subgraph output tensor metadata mismatch" ): _load_model_from_buffer(_build_tflite_call_model(callee_output_shape=[2])) def _build_tflite_if_model( condition_type=_tfl_tensor_type.BOOL, then_subgraph_index=1, else_subgraph_index=2, then_outputs=None, else_outputs=None, else_input_shape=None, else_input_type=None, else_output_shape=None, else_output_type=None, ): """Build a TFLite model where IF selects x + 1 or x - 1.""" builder = flatbuffers.Builder(1024) then_outputs = [2] if then_outputs is None else then_outputs else_outputs = [2] if else_outputs is None else else_outputs else_input_shape = [2, 2] if else_input_shape is None else else_input_shape else_input_type = _tfl_tensor_type.FLOAT32 if else_input_type is None else else_input_type else_output_shape = [2, 2] if else_output_shape is None else else_output_shape else_output_type = _tfl_tensor_type.FLOAT32 if else_output_type is None else else_output_type if_options = _build_if_options(builder, then_subgraph_index, else_subgraph_index) one = np.array(1.0, dtype=np.float32) main_tensors = [ _build_tensor(builder, 0, [], tensor_type=condition_type), _build_tensor(builder, 1, [2, 2]), _build_tensor(builder, 3, [2, 2]), ] main_if = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.IfOptions, builtin_options=if_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_if], inputs=[0, 1], outputs=[2], ) then_tensors = [ _build_tensor(builder, 1, [2, 2]), _build_tensor(builder, 2, []), _build_tensor(builder, 3, [2, 2]), ] then_add = _build_operator(builder, 1, [0, 1], [2]) then_subgraph = _build_subgraph( builder, tensors=then_tensors, operators=[then_add], inputs=[0], outputs=then_outputs, ) else_tensors = [ _build_tensor(builder, 1, else_input_shape, tensor_type=else_input_type), _build_tensor(builder, 2, []), _build_tensor(builder, 3, else_output_shape, tensor_type=else_output_type), ] else_sub = _build_operator(builder, 2, [0, 1], [2]) else_subgraph = _build_subgraph( builder, tensors=else_tensors, operators=[else_sub], inputs=[0], outputs=else_outputs, ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("IF")), _build_operator_code(builder, _get_builtin_operator("ADD")), _build_operator_code(builder, _get_builtin_operator("SUB")), ] buffers = [ _build_buffer(builder), _build_buffer(builder), _build_buffer(builder, one.tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[then_subgraph, else_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_if_subgraphs(): """Test TFLite IF conversion to Relax If.""" mod = _load_model_from_buffer(_build_tflite_if_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_if_then_subgraph_1( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add( tvmgen_tensor_0, R.const(1.0, "float32") ) R.output(gv) return gv @R.function(private=True) def tflite_if_else_subgraph_2( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.subtract( tvmgen_tensor_0, R.const(1.0, "float32") ) R.output(gv) return gv @R.function(private=True) def tflite_if_subgraph_1_2( tvmgen_tensor_0: R.Tensor((), dtype="bool"), tvmgen_tensor_1: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): cls = Expected if tvmgen_tensor_0: gv: R.Tensor((2, 2), dtype="float32") = cls.tflite_if_then_subgraph_1( tvmgen_tensor_1 ) cond_result: R.Tensor((2, 2), dtype="float32") = gv else: gv1: R.Tensor((2, 2), dtype="float32") = cls.tflite_if_else_subgraph_2( tvmgen_tensor_1 ) cond_result: R.Tensor((2, 2), dtype="float32") = gv1 return cond_result @R.function def main( tvmgen_tensor_0: R.Tensor((), dtype="bool"), tvmgen_tensor_1: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 2}) cls = Expected with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = cls.tflite_if_subgraph_1_2( tvmgen_tensor_0, tvmgen_tensor_1 ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_tflite_multi_output_if_model(): """Build a TFLite model where IF returns two tensor outputs.""" builder = flatbuffers.Builder(1024) if_options = _build_if_options(builder, 1, 2) one = np.array(1.0, dtype=np.float32) main_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.BOOL), _build_tensor(builder, 1, [2, 2]), _build_tensor(builder, 4, [2, 2]), _build_tensor(builder, 5, [2, 2]), ] main_if = _build_operator( builder, 0, [0, 1], [2, 3], builtin_options_type=_tfl_builtin_options.IfOptions, builtin_options=if_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_if], inputs=[0, 1], outputs=[2, 3], ) then_tensors = [ _build_tensor(builder, 1, [2, 2]), _build_tensor(builder, 2, []), _build_tensor(builder, 3, [2, 2]), _build_tensor(builder, 4, [2, 2]), ] then_add = _build_operator(builder, 1, [0, 1], [2]) then_sub = _build_operator(builder, 2, [0, 1], [3]) then_subgraph = _build_subgraph( builder, tensors=then_tensors, operators=[then_add, then_sub], inputs=[0], outputs=[2, 3], ) else_tensors = [ _build_tensor(builder, 1, [2, 2]), _build_tensor(builder, 2, []), _build_tensor(builder, 3, [2, 2]), _build_tensor(builder, 4, [2, 2]), ] else_sub = _build_operator(builder, 2, [0, 1], [2]) else_add = _build_operator(builder, 1, [0, 1], [3]) else_subgraph = _build_subgraph( builder, tensors=else_tensors, operators=[else_sub, else_add], inputs=[0], outputs=[2, 3], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("IF")), _build_operator_code(builder, _get_builtin_operator("ADD")), _build_operator_code(builder, _get_builtin_operator("SUB")), ] buffers = [ _build_buffer(builder), _build_buffer(builder), _build_buffer(builder, one.tobytes()), _build_buffer(builder), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[then_subgraph, else_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_if_subgraphs_multi_output(): """Test IF tuple returns are preserved through the private wrapper function.""" mod = _load_model_from_buffer(_build_tflite_multi_output_if_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_if_then_subgraph_1( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add( tvmgen_tensor_0, R.const(1.0, "float32") ) gv1: R.Tensor((2, 2), dtype="float32") = R.subtract( tvmgen_tensor_0, R.const(1.0, "float32") ) gv2: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = (gv, gv1) R.output(gv2) return gv2 @R.function(private=True) def tflite_if_else_subgraph_2( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.subtract( tvmgen_tensor_0, R.const(1.0, "float32") ) gv1: R.Tensor((2, 2), dtype="float32") = R.add( tvmgen_tensor_0, R.const(1.0, "float32") ) gv2: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = (gv, gv1) R.output(gv2) return gv2 @R.function(private=True) def tflite_if_subgraph_1_2( tvmgen_tensor_0: R.Tensor((), dtype="bool"), tvmgen_tensor_1: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): cls = Expected if tvmgen_tensor_0: gv: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = cls.tflite_if_then_subgraph_1(tvmgen_tensor_1) cond_result: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = gv else: gv1: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = cls.tflite_if_else_subgraph_2(tvmgen_tensor_1) cond_result: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = gv1 return cond_result @R.function def main( tvmgen_tensor_0: R.Tensor((), dtype="bool"), tvmgen_tensor_1: R.Tensor((2, 2), dtype="float32"), ) -> R.Tuple(R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32")): R.func_attr({"num_input": 2}) cls = Expected with R.dataflow(): lv: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = cls.tflite_if_subgraph_1_2(tvmgen_tensor_0, tvmgen_tensor_1) lv1: R.Tensor((2, 2), dtype="float32") = lv[0] lv2: R.Tensor((2, 2), dtype="float32") = lv[1] gv: R.Tuple( R.Tensor((2, 2), dtype="float32"), R.Tensor((2, 2), dtype="float32") ) = (lv1, lv2) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_if_subgraphs_non_bool_condition_unsupported(): """Test IF rejects non-bool condition tensors.""" with pytest.raises(tvm.error.OpNotImplemented, match="IF requires a scalar bool condition"): _load_model_from_buffer(_build_tflite_if_model(condition_type=_tfl_tensor_type.INT32)) def test_if_subgraphs_invalid_index_unsupported(): """Test IF rejects invalid branch subgraph indices before lowering.""" with pytest.raises(tvm.error.OpNotImplemented, match="IF requires a valid subgraph index"): _load_model_from_buffer(_build_tflite_if_model(then_subgraph_index=3)) def test_if_subgraphs_output_count_mismatch_unsupported(): """Test IF rejects branches whose output arity does not match the call site.""" with pytest.raises(tvm.error.OpNotImplemented, match="IF subgraph output count mismatch"): _load_model_from_buffer(_build_tflite_if_model(else_outputs=[])) def test_if_subgraphs_input_metadata_mismatch_unsupported(): """Test IF rejects branches whose input metadata does not match the call site.""" with pytest.raises( tvm.error.OpNotImplemented, match="IF subgraph input tensor metadata mismatch" ): _load_model_from_buffer(_build_tflite_if_model(else_input_shape=[2])) def test_if_subgraphs_output_metadata_mismatch_unsupported(): """Test IF rejects branches whose output metadata does not match the call site.""" with pytest.raises( tvm.error.OpNotImplemented, match="IF subgraph output tensor metadata mismatch" ): _load_model_from_buffer(_build_tflite_if_model(else_output_shape=[2])) def _build_tflite_while_model( cond_subgraph_index=1, body_subgraph_index=2, cond_output_type=_tfl_tensor_type.BOOL, cond_input_type=_tfl_tensor_type.INT32, body_outputs=None, body_input_type=_tfl_tensor_type.INT32, body_output_type=_tfl_tensor_type.INT32, main_output_type=_tfl_tensor_type.INT32, ): """Build a TFLite WHILE model incrementing an int32 scalar until i < 3 is false.""" builder = flatbuffers.Builder(1024) body_outputs = [2] if body_outputs is None else body_outputs while_options = _build_while_options(builder, cond_subgraph_index, body_subgraph_index) one = np.array(1, dtype=np.int32) three = np.array(3, dtype=np.int32) main_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=main_output_type), ] main_while = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.WhileOptions, builtin_options=while_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_while], inputs=[0], outputs=[1], ) cond_tensors = [ _build_tensor(builder, 0, [], tensor_type=cond_input_type), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=cond_output_type), ] cond_less = _build_operator(builder, 1, [0, 1], [2]) cond_subgraph = _build_subgraph( builder, tensors=cond_tensors, operators=[cond_less], inputs=[0], outputs=[2], ) body_tensors = [ _build_tensor(builder, 0, [], tensor_type=body_input_type), _build_tensor(builder, 2, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=body_output_type), ] body_add = _build_operator(builder, 2, [0, 1], [2]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[body_add], inputs=[0], outputs=body_outputs, ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("WHILE")), _build_operator_code(builder, _get_builtin_operator("LESS")), _build_operator_code(builder, _get_builtin_operator("ADD")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, three.tobytes()), _build_buffer(builder, one.tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[cond_subgraph, body_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_repeated_while_model(): """Build a TFLite model where two WHILE ops share the same cond/body subgraphs.""" builder = flatbuffers.Builder(1024) while_options = _build_while_options(builder, 1, 2) one = np.array(1, dtype=np.int32) three = np.array(3, dtype=np.int32) main_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 4, [], tensor_type=_tfl_tensor_type.INT32), ] main_while_0 = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.WhileOptions, builtin_options=while_options, ) main_while_1 = _build_operator( builder, 0, [1], [2], builtin_options_type=_tfl_builtin_options.WhileOptions, builtin_options=while_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_while_0, main_while_1], inputs=[0], outputs=[2], ) cond_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=_tfl_tensor_type.BOOL), ] cond_less = _build_operator(builder, 1, [0, 1], [2]) cond_subgraph = _build_subgraph( builder, tensors=cond_tensors, operators=[cond_less], inputs=[0], outputs=[2], ) body_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 2, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=_tfl_tensor_type.INT32), ] body_add = _build_operator(builder, 2, [0, 1], [2]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[body_add], inputs=[0], outputs=[2], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("WHILE")), _build_operator_code(builder, _get_builtin_operator("LESS")), _build_operator_code(builder, _get_builtin_operator("ADD")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, three.tobytes()), _build_buffer(builder, one.tobytes()), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[cond_subgraph, body_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_zero_var_while_model(): """Build a TFLite WHILE model with no loop-carried tensors.""" builder = flatbuffers.Builder(1024) while_options = _build_while_options(builder, 1, 2) main_while = _build_operator( builder, 0, [], [], builtin_options_type=_tfl_builtin_options.WhileOptions, builtin_options=while_options, ) main_subgraph = _build_subgraph( builder, tensors=[], operators=[main_while], inputs=[], outputs=[], ) cond_subgraph = _build_subgraph(builder, tensors=[], operators=[], inputs=[], outputs=[]) body_subgraph = _build_subgraph(builder, tensors=[], operators=[], inputs=[], outputs=[]) operator_codes = [_build_operator_code(builder, _get_builtin_operator("WHILE"))] buffers = [_build_buffer(builder)] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[cond_subgraph, body_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_while_subgraphs(): """Test TFLite WHILE conversion to a recursive Relax private function.""" mod = _load_model_from_buffer(_build_tflite_while_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_while_cond_subgraph_1( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="bool"): with R.dataflow(): gv: R.Tensor((), dtype="bool") = R.less(tvmgen_tensor_0, R.const(3, "int32")) R.output(gv) return gv @R.function(private=True) def tflite_while_body_subgraph_2( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="int32"): with R.dataflow(): gv: R.Tensor((), dtype="int32") = R.add(tvmgen_tensor_0, R.const(1, "int32")) R.output(gv) return gv @R.function(private=True) def tflite_while_subgraph_1_2( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="int32"): cls = Expected while_cond: R.Tensor((), dtype="bool") = cls.tflite_while_cond_subgraph_1( tvmgen_tensor_0 ) if while_cond: gv: R.Tensor((), dtype="int32") = cls.tflite_while_body_subgraph_2(tvmgen_tensor_0) gv1: R.Tensor((), dtype="int32") = cls.tflite_while_subgraph_1_2(gv) cond_result: R.Tensor((), dtype="int32") = gv1 else: cond_result: R.Tensor((), dtype="int32") = tvmgen_tensor_0 return cond_result @R.function def main( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="int32"): R.func_attr({"num_input": 1}) cls = Expected with R.dataflow(): gv: R.Tensor((), dtype="int32") = cls.tflite_while_subgraph_1_2(tvmgen_tensor_0) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_while_subgraphs_repeated_cond_body_pair(): """Test repeated WHILE ops reuse the same recursive private function.""" mod = _load_model_from_buffer(_build_tflite_repeated_while_model()) names = [gv.name_hint for gv in mod.get_global_vars()] assert names.count("tflite_while_subgraph_1_2") == 1 tvm.ir.assert_structural_equal(mod["main"].ret_ty, relax.TensorType((), "int32")) tvm.ir.assert_structural_equal( mod["tflite_while_subgraph_1_2"].ret_ty, relax.TensorType((), "int32"), ) def _build_tflite_two_var_while_model(): """Build a TFLite WHILE model with two int32 loop-carried scalar tensors.""" builder = flatbuffers.Builder(1024) while_options = _build_while_options(builder, 1, 2) one = np.array(1, dtype=np.int32) three = np.array(3, dtype=np.int32) main_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 4, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 5, [], tensor_type=_tfl_tensor_type.INT32), ] main_while = _build_operator( builder, 0, [0, 1], [2, 3], builtin_options_type=_tfl_builtin_options.WhileOptions, builtin_options=while_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_while], inputs=[0, 1], outputs=[2, 3], ) cond_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 2, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 4, [], tensor_type=_tfl_tensor_type.BOOL), ] cond_less = _build_operator(builder, 1, [0, 2], [3]) cond_subgraph = _build_subgraph( builder, tensors=cond_tensors, operators=[cond_less], inputs=[0, 1], outputs=[3], ) body_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 4, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 5, [], tensor_type=_tfl_tensor_type.INT32), ] body_add_i = _build_operator(builder, 2, [0, 2], [3]) body_add_acc = _build_operator(builder, 2, [1, 0], [4]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[body_add_i, body_add_acc], inputs=[0, 1], outputs=[3, 4], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("WHILE")), _build_operator_code(builder, _get_builtin_operator("LESS")), _build_operator_code(builder, _get_builtin_operator("ADD")), ] buffers = [ _build_buffer(builder), _build_buffer(builder), _build_buffer(builder, three.tobytes()), _build_buffer(builder, one.tobytes()), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[cond_subgraph, body_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_while_subgraphs_two_loop_vars(): """Test WHILE tuple loop state with two loop-carried variables.""" mod = _load_model_from_buffer(_build_tflite_two_var_while_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_while_cond_subgraph_1( tvmgen_tensor_0: R.Tensor((), dtype="int32"), tvmgen_tensor_1: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="bool"): with R.dataflow(): gv: R.Tensor((), dtype="bool") = R.less(tvmgen_tensor_0, R.const(3, "int32")) R.output(gv) return gv @R.function(private=True) def tflite_while_body_subgraph_2( tvmgen_tensor_0: R.Tensor((), dtype="int32"), tvmgen_tensor_1: R.Tensor((), dtype="int32"), ) -> R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")): with R.dataflow(): gv: R.Tensor((), dtype="int32") = R.add(tvmgen_tensor_0, R.const(1, "int32")) gv1: R.Tensor((), dtype="int32") = R.add(tvmgen_tensor_1, tvmgen_tensor_0) gv2: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = ( gv, gv1, ) R.output(gv2) return gv2 @R.function(private=True) def tflite_while_subgraph_1_2( tvmgen_tensor_0: R.Tensor((), dtype="int32"), tvmgen_tensor_1: R.Tensor((), dtype="int32"), ) -> R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")): cls = Expected while_cond: R.Tensor((), dtype="bool") = cls.tflite_while_cond_subgraph_1( tvmgen_tensor_0, tvmgen_tensor_1 ) if while_cond: gv: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = ( cls.tflite_while_body_subgraph_2(tvmgen_tensor_0, tvmgen_tensor_1) ) gv1: R.Tensor((), dtype="int32") = gv[0] gv2: R.Tensor((), dtype="int32") = gv[1] gv3: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = ( cls.tflite_while_subgraph_1_2(gv1, gv2) ) cond_result: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = gv3 else: cond_result: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = ( tvmgen_tensor_0, tvmgen_tensor_1, ) return cond_result @R.function def main( tvmgen_tensor_0: R.Tensor((), dtype="int32"), tvmgen_tensor_1: R.Tensor((), dtype="int32"), ) -> R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")): R.func_attr({"num_input": 2}) cls = Expected with R.dataflow(): lv: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = ( cls.tflite_while_subgraph_1_2(tvmgen_tensor_0, tvmgen_tensor_1) ) lv1: R.Tensor((), dtype="int32") = lv[0] lv2: R.Tensor((), dtype="int32") = lv[1] gv: R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((), dtype="int32")) = ( lv1, lv2, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_while_subgraphs_non_bool_condition_unsupported(): """Test WHILE rejects cond subgraphs that do not return scalar bool.""" with pytest.raises(tvm.error.OpNotImplemented, match="WHILE requires a scalar bool condition"): _load_model_from_buffer(_build_tflite_while_model(cond_output_type=_tfl_tensor_type.INT32)) def test_while_subgraphs_invalid_index_unsupported(): """Test WHILE rejects invalid cond/body subgraph indices before lowering.""" with pytest.raises(tvm.error.OpNotImplemented, match="WHILE requires a valid subgraph index"): _load_model_from_buffer(_build_tflite_while_model(cond_subgraph_index=3)) def test_while_subgraphs_zero_loop_vars_unsupported(): """Test WHILE rejects operators without loop-carried tensors.""" with pytest.raises(tvm.error.OpNotImplemented, match="WHILE requires loop-carried inputs"): _load_model_from_buffer(_build_tflite_zero_var_while_model()) def test_while_subgraphs_loop_state_metadata_mismatch_unsupported(): """Test WHILE rejects loop outputs whose metadata does not match loop inputs.""" with pytest.raises( tvm.error.OpNotImplemented, match="WHILE loop state tensor metadata mismatch" ): _load_model_from_buffer( _build_tflite_while_model(main_output_type=_tfl_tensor_type.FLOAT32) ) def test_while_subgraphs_output_count_mismatch_unsupported(): """Test WHILE rejects body subgraphs whose output arity does not match loop vars.""" with pytest.raises(tvm.error.OpNotImplemented, match="WHILE subgraph output count mismatch"): _load_model_from_buffer(_build_tflite_while_model(body_outputs=[])) def test_while_subgraphs_input_metadata_mismatch_unsupported(): """Test WHILE rejects cond subgraph inputs whose metadata does not match loop vars.""" with pytest.raises( tvm.error.OpNotImplemented, match="WHILE subgraph input tensor metadata mismatch" ): _load_model_from_buffer(_build_tflite_while_model(cond_input_type=_tfl_tensor_type.FLOAT32)) def test_while_subgraphs_output_metadata_mismatch_unsupported(): """Test WHILE rejects body outputs whose metadata does not match loop vars.""" with pytest.raises( tvm.error.OpNotImplemented, match="WHILE subgraph output tensor metadata mismatch" ): _load_model_from_buffer( _build_tflite_while_model(body_output_type=_tfl_tensor_type.FLOAT32) ) def _build_tflite_call_once_model( init_has_op=False, init_subgraph_index=1, call_once_inputs=None, call_once_outputs=None, init_inputs=None, init_outputs=None, ): """Build a TFLite model with CALL_ONCE and one pass-through output.""" builder = flatbuffers.Builder(1024) call_once_inputs = [] if call_once_inputs is None else call_once_inputs call_once_outputs = [] if call_once_outputs is None else call_once_outputs init_inputs = [] if init_inputs is None else init_inputs init_outputs = [] if init_outputs is None else init_outputs call_once_options = _build_call_once_options(builder, init_subgraph_index) main_tensors = [_build_tensor(builder, 0, [2, 2])] main_call_once = _build_operator( builder, 0, call_once_inputs, call_once_outputs, builtin_options_type=_tfl_builtin_options.CallOnceOptions, builtin_options=call_once_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_call_once], inputs=[0], outputs=[0], ) if init_has_op: one = np.array(1.0, dtype=np.float32) init_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, []), _build_tensor(builder, 2, [2, 2]), ] init_op = _build_operator(builder, 1, [0, 1], [2]) buffers = [ _build_buffer(builder), _build_buffer(builder, one.tobytes()), _build_buffer(builder), ] else: init_tensors = ( [_build_tensor(builder, 0, [2, 2])] if len(init_inputs) != 0 or len(init_outputs) != 0 else [] ) init_op = None buffers = [_build_buffer(builder)] init_subgraph = _build_subgraph( builder, tensors=init_tensors, operators=[] if init_op is None else [init_op], inputs=init_inputs, outputs=init_outputs, ) operator_codes = [_build_operator_code(builder, _get_builtin_operator("CALL_ONCE"))] if init_has_op: operator_codes.append(_build_operator_code(builder, _get_builtin_operator("ADD"))) return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[init_subgraph], operator_codes=operator_codes, buffers=buffers, ) def test_call_once_empty_init_subgraph(): """Test the no-op CALL_ONCE subset.""" mod = _load_model_from_buffer(_build_tflite_call_once_model()) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = tvmgen_tensor_0 R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_call_once_non_empty_init_subgraph_unsupported(): """Test CALL_ONCE rejects init subgraphs with side-effect-like bodies.""" with pytest.raises(tvm.error.OpNotImplemented, match="CALL_ONCE"): _load_model_from_buffer(_build_tflite_call_once_model(init_has_op=True)) def test_call_once_inputs_outputs_unsupported(): """Test CALL_ONCE rejects operator inputs and outputs.""" with pytest.raises(tvm.error.OpNotImplemented, match="CALL_ONCE with inputs or outputs"): _load_model_from_buffer( _build_tflite_call_once_model(call_once_inputs=[0], call_once_outputs=[0]) ) def test_call_once_init_subgraph_io_unsupported(): """Test CALL_ONCE rejects init subgraphs with inputs or outputs.""" with pytest.raises( tvm.error.OpNotImplemented, match="CALL_ONCE with non-empty init subgraph I/O" ): _load_model_from_buffer(_build_tflite_call_once_model(init_inputs=[0], init_outputs=[0])) def test_call_once_invalid_index_unsupported(): """Test CALL_ONCE rejects invalid init subgraph indices before lowering.""" with pytest.raises( tvm.error.OpNotImplemented, match="CALL_ONCE requires a valid subgraph index" ): _load_model_from_buffer(_build_tflite_call_once_model(init_subgraph_index=2)) def _build_tflite_resource_variable_model(): """Build a model that initializes a resource variable in CALL_ONCE and reads it.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() initial_value = np.array([1.0, 2.0], dtype=np.float32) call_once_options = _build_call_once_options(builder, 1) main_var_handle_options = _build_var_handle_options(builder) main_read_options = _build_empty_builtin_options(builder, "ReadVariableOptions") init_var_handle_options = _build_var_handle_options(builder) init_assign_options = _build_empty_builtin_options(builder, "AssignVariableOptions") resource_tensor = _build_tensor(builder, 0, [], tensor_type=resource_type) main_output_tensor = _build_tensor(builder, 0, [2]) main_call_once = _build_operator( builder, 0, [], [], builtin_options_type=_get_builtin_options_type("CallOnceOptions"), builtin_options=call_once_options, ) main_var_handle = _build_operator( builder, 1, [], [0], builtin_options_type=_get_builtin_options_type("VarHandleOptions"), builtin_options=main_var_handle_options, ) main_read = _build_operator( builder, 2, [0], [1], builtin_options_type=_get_builtin_options_type("ReadVariableOptions"), builtin_options=main_read_options, ) main_subgraph = _build_subgraph( builder, tensors=[resource_tensor, main_output_tensor], operators=[main_call_once, main_var_handle, main_read], inputs=[], outputs=[1], ) init_resource_tensor = _build_tensor(builder, 0, [], tensor_type=resource_type) init_value_tensor = _build_tensor(builder, 1, [2]) init_var_handle = _build_operator( builder, 1, [], [0], builtin_options_type=_get_builtin_options_type("VarHandleOptions"), builtin_options=init_var_handle_options, ) init_assign = _build_operator( builder, 3, [0, 1], [], builtin_options_type=_get_builtin_options_type("AssignVariableOptions"), builtin_options=init_assign_options, ) init_subgraph = _build_subgraph( builder, tensors=[init_resource_tensor, init_value_tensor], operators=[init_var_handle, init_assign], inputs=[], outputs=[], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL_ONCE")), _build_operator_code(builder, _get_builtin_operator("VAR_HANDLE")), _build_operator_code(builder, _get_builtin_operator("READ_VARIABLE")), _build_operator_code(builder, _get_builtin_operator("ASSIGN_VARIABLE")), ] buffers = [_build_buffer(builder), _build_buffer(builder, initial_value.tobytes())] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[init_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_resource_assign_in_main_model(): """Build a model that attempts to assign a resource variable in the main subgraph.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() value = np.array([1.0, 2.0], dtype=np.float32) var_handle_options = _build_var_handle_options(builder) assign_options = _build_empty_builtin_options(builder, "AssignVariableOptions") resource_tensor = _build_tensor(builder, 0, [], tensor_type=resource_type) value_tensor = _build_tensor(builder, 1, [2]) var_handle = _build_operator( builder, 0, [], [0], builtin_options_type=_get_builtin_options_type("VarHandleOptions"), builtin_options=var_handle_options, ) assign = _build_operator( builder, 1, [0, 1], [], builtin_options_type=_get_builtin_options_type("AssignVariableOptions"), builtin_options=assign_options, ) main_subgraph = _build_subgraph( builder, tensors=[resource_tensor, value_tensor], operators=[var_handle, assign], inputs=[], outputs=[1], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("VAR_HANDLE")), _build_operator_code(builder, _get_builtin_operator("ASSIGN_VARIABLE")), ] buffers = [_build_buffer(builder), _build_buffer(builder, value.tobytes())] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_resource_read_uninitialized_model(): """Build a model that reads a resource variable without CALL_ONCE initialization.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() var_handle_options = _build_var_handle_options(builder) read_options = _build_empty_builtin_options(builder, "ReadVariableOptions") resource_tensor = _build_tensor(builder, 0, [], tensor_type=resource_type) output_tensor = _build_tensor(builder, 0, [2]) var_handle = _build_operator( builder, 0, [], [0], builtin_options_type=_get_builtin_options_type("VarHandleOptions"), builtin_options=var_handle_options, ) read = _build_operator( builder, 1, [0], [1], builtin_options_type=_get_builtin_options_type("ReadVariableOptions"), builtin_options=read_options, ) main_subgraph = _build_subgraph( builder, tensors=[resource_tensor, output_tensor], operators=[var_handle, read], inputs=[], outputs=[1], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("VAR_HANDLE")), _build_operator_code(builder, _get_builtin_operator("READ_VARIABLE")), ] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)], ) def _build_tflite_hashtable_find_string_to_int64_model( query_values=None, query_shape=None, default_values=None, default_shape=None, table_keys=None, table_values=None, query_is_input=False, ): """Build a static string-to-int64 HASHTABLE_FIND model.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() string_type = _get_string_tensor_type() query_values = ["alpha", "missing", "beta"] if query_values is None else query_values query_shape = [len(query_values)] if query_shape is None else query_shape default_shape = [] if default_shape is None else default_shape table_keys = ["alpha", "beta", "gamma"] if table_keys is None else table_keys table_values = ( np.array([10, 20, 30], dtype=np.int64) if table_values is None else np.array(table_values, dtype=np.int64) ) default_values = ( np.array(-1, dtype=np.int64) if default_values is None else np.array(default_values, dtype=np.int64) ) query_buffer = _build_tflite_string_buffer(query_values) table_key_buffer = _build_tflite_string_buffer(table_keys) call_once_options = _build_call_once_options(builder, 1) main_table_options = _build_hashtable_options( builder, table_id=0, key_dtype=string_type, value_dtype=_tfl_tensor_type.INT64, ) find_options = _build_empty_builtin_options(builder, "HashtableFindOptions") init_table_options = _build_hashtable_options( builder, table_id=0, key_dtype=string_type, value_dtype=_tfl_tensor_type.INT64, ) import_options = _build_empty_builtin_options(builder, "HashtableImportOptions") query_buffer_idx = 0 if query_is_input else 1 query_tensor = _build_tensor(builder, query_buffer_idx, query_shape, tensor_type=string_type) table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) default_tensor = _build_tensor(builder, 2, default_shape, tensor_type=_tfl_tensor_type.INT64) output_tensor = _build_tensor(builder, 0, query_shape, tensor_type=_tfl_tensor_type.INT64) main_call_once = _build_operator( builder, 0, [], [], builtin_options_type=_get_builtin_options_type("CallOnceOptions"), builtin_options=call_once_options, ) main_hashtable = _build_operator( builder, 1, [], [1], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=main_table_options, ) main_find = _build_operator( builder, 2, [1, 0, 2], [3], builtin_options_type=_get_builtin_options_type("HashtableFindOptions"), builtin_options=find_options, ) main_subgraph = _build_subgraph( builder, tensors=[query_tensor, table_tensor, default_tensor, output_tensor], operators=[main_call_once, main_hashtable, main_find], inputs=[0] if query_is_input else [], outputs=[3], ) init_table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) init_keys_tensor = _build_tensor(builder, 3, [len(table_keys)], tensor_type=string_type) init_values_tensor = _build_tensor( builder, 4, [len(table_values)], tensor_type=_tfl_tensor_type.INT64, ) init_hashtable = _build_operator( builder, 1, [], [0], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=init_table_options, ) init_import = _build_operator( builder, 3, [0, 1, 2], [], builtin_options_type=_get_builtin_options_type("HashtableImportOptions"), builtin_options=import_options, ) init_subgraph = _build_subgraph( builder, tensors=[init_table_tensor, init_keys_tensor, init_values_tensor], operators=[init_hashtable, init_import], inputs=[], outputs=[], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL_ONCE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_FIND")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_IMPORT")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, b"" if query_is_input else query_buffer), _build_buffer(builder, default_values.tobytes()), _build_buffer(builder, table_key_buffer), _build_buffer(builder, table_values.tobytes()), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[init_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_hashtable_find_int64_to_string_model(): """Build a static int64-to-string HASHTABLE_FIND model.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() string_type = _get_string_tensor_type() query_values = np.array([10, 30], dtype=np.int64) table_keys = np.array([10, 20], dtype=np.int64) table_values = _build_tflite_string_buffer(["ten", "twenty"]) default_value = _build_tflite_string_buffer(["missing"]) call_once_options = _build_call_once_options(builder, 1) main_table_options = _build_hashtable_options(builder, table_id=0) find_options = _build_empty_builtin_options(builder, "HashtableFindOptions") init_table_options = _build_hashtable_options(builder, table_id=0) import_options = _build_empty_builtin_options(builder, "HashtableImportOptions") query_tensor = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT64) table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) default_tensor = _build_tensor(builder, 2, [], tensor_type=string_type) output_tensor = _build_tensor(builder, 0, [2], tensor_type=string_type) main_call_once = _build_operator( builder, 0, [], [], builtin_options_type=_get_builtin_options_type("CallOnceOptions"), builtin_options=call_once_options, ) main_hashtable = _build_operator( builder, 1, [], [1], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=main_table_options, ) main_find = _build_operator( builder, 2, [1, 0, 2], [3], builtin_options_type=_get_builtin_options_type("HashtableFindOptions"), builtin_options=find_options, ) main_subgraph = _build_subgraph( builder, tensors=[query_tensor, table_tensor, default_tensor, output_tensor], operators=[main_call_once, main_hashtable, main_find], inputs=[], outputs=[3], ) init_table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) init_keys_tensor = _build_tensor(builder, 3, [2], tensor_type=_tfl_tensor_type.INT64) init_values_tensor = _build_tensor(builder, 4, [2], tensor_type=string_type) init_hashtable = _build_operator( builder, 1, [], [0], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=init_table_options, ) init_import = _build_operator( builder, 3, [0, 1, 2], [], builtin_options_type=_get_builtin_options_type("HashtableImportOptions"), builtin_options=import_options, ) init_subgraph = _build_subgraph( builder, tensors=[init_table_tensor, init_keys_tensor, init_values_tensor], operators=[init_hashtable, init_import], inputs=[], outputs=[], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL_ONCE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_FIND")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_IMPORT")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, query_values.tobytes()), _build_buffer(builder, default_value), _build_buffer(builder, table_keys.tobytes()), _build_buffer(builder, table_values), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[init_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_hashtable_size_model(): """Build a model that imports a static hashtable and returns its size.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() string_type = _get_string_tensor_type() table_keys = np.array([10, 20], dtype=np.int64) table_values = _build_tflite_string_buffer(["one hundred", "two hundred"]) call_once_options = _build_call_once_options(builder, 1) main_table_options = _build_hashtable_options(builder, table_id=0) size_options = _build_empty_builtin_options(builder, "HashtableSizeOptions") init_table_options = _build_hashtable_options(builder, table_id=0) import_options = _build_empty_builtin_options(builder, "HashtableImportOptions") table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) size_tensor = _build_tensor(builder, 0, [1], tensor_type=_tfl_tensor_type.INT64) main_call_once = _build_operator( builder, 0, [], [], builtin_options_type=_get_builtin_options_type("CallOnceOptions"), builtin_options=call_once_options, ) main_hashtable = _build_operator( builder, 1, [], [0], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=main_table_options, ) main_size = _build_operator( builder, 2, [0], [1], builtin_options_type=_get_builtin_options_type("HashtableSizeOptions"), builtin_options=size_options, ) main_subgraph = _build_subgraph( builder, tensors=[table_tensor, size_tensor], operators=[main_call_once, main_hashtable, main_size], inputs=[], outputs=[1], ) init_table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) init_keys_tensor = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT64) init_values_tensor = _build_tensor(builder, 2, [2], tensor_type=string_type) init_hashtable = _build_operator( builder, 1, [], [0], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=init_table_options, ) init_import = _build_operator( builder, 3, [0, 1, 2], [], builtin_options_type=_get_builtin_options_type("HashtableImportOptions"), builtin_options=import_options, ) init_subgraph = _build_subgraph( builder, tensors=[init_table_tensor, init_keys_tensor, init_values_tensor], operators=[init_hashtable, init_import], inputs=[], outputs=[], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("CALL_ONCE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_SIZE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_IMPORT")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, table_keys.tobytes()), _build_buffer(builder, table_values), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[init_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_hashtable_import_in_main_model(): """Build a model that attempts to import hashtable values in the main subgraph.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() string_type = _get_string_tensor_type() table_keys = np.array([10, 20], dtype=np.int64) table_values = _build_tflite_string_buffer(["one hundred", "two hundred"]) table_options = _build_hashtable_options(builder, table_id=0) import_options = _build_empty_builtin_options(builder, "HashtableImportOptions") table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) keys_tensor = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT64) values_tensor = _build_tensor(builder, 2, [2], tensor_type=string_type) hashtable = _build_operator( builder, 0, [], [0], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=table_options, ) hashtable_import = _build_operator( builder, 1, [0, 1, 2], [], builtin_options_type=_get_builtin_options_type("HashtableImportOptions"), builtin_options=import_options, ) main_subgraph = _build_subgraph( builder, tensors=[table_tensor, keys_tensor, values_tensor], operators=[hashtable, hashtable_import], inputs=[], outputs=[2], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("HASHTABLE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_IMPORT")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, table_keys.tobytes()), _build_buffer(builder, table_values), ] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_hashtable_size_uninitialized_model(): """Build a model that queries the size of a hashtable without importing values.""" builder = flatbuffers.Builder(1024) resource_type = _get_resource_tensor_type() table_options = _build_hashtable_options(builder, table_id=0) size_options = _build_empty_builtin_options(builder, "HashtableSizeOptions") table_tensor = _build_tensor(builder, 0, [1], tensor_type=resource_type) size_tensor = _build_tensor(builder, 0, [1], tensor_type=_tfl_tensor_type.INT64) hashtable = _build_operator( builder, 0, [], [0], builtin_options_type=_get_builtin_options_type("HashtableOptions"), builtin_options=table_options, ) hashtable_size = _build_operator( builder, 1, [0], [1], builtin_options_type=_get_builtin_options_type("HashtableSizeOptions"), builtin_options=size_options, ) main_subgraph = _build_subgraph( builder, tensors=[table_tensor, size_tensor], operators=[hashtable, hashtable_size], inputs=[], outputs=[1], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("HASHTABLE")), _build_operator_code(builder, _get_builtin_operator("HASHTABLE_SIZE")), ] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)], ) def _build_tflite_embedding_lookup_sparse_model( combiner, indices_data, dense_shape_data, weights_data=None ): builder = flatbuffers.Builder(4096) ids_data = np.array([1, 3, 0], dtype=np.int32) indices_data = np.array(indices_data, dtype=np.int32) dense_shape_data = np.array(dense_shape_data, dtype=np.int32) weights_data = ( np.array([1.0, 2.0, 4.0], dtype=np.float32) if weights_data is None else np.array(weights_data, dtype=np.float32) ) params_data = np.array( [ [[0.00, 0.01], [0.10, 0.11], [0.20, 0.21]], [[1.00, 1.01], [1.10, 1.11], [1.20, 1.21]], [[2.00, 2.01], [2.10, 2.11], [2.20, 2.21]], [[3.00, 3.01], [3.10, 3.11], [3.20, 3.21]], ], dtype=np.float32, ) output_shape = dense_shape_data[:-1].tolist() + list(params_data.shape[1:]) sparse_options = _build_embedding_lookup_sparse_options(builder, combiner) ids_tensor = _build_tensor(builder, 0, list(ids_data.shape), tensor_type=_tfl_tensor_type.INT32) indices_tensor = _build_tensor( builder, 1, list(indices_data.shape), tensor_type=_tfl_tensor_type.INT32 ) dense_shape_tensor = _build_tensor( builder, 2, list(dense_shape_data.shape), tensor_type=_tfl_tensor_type.INT32 ) weights_tensor = _build_tensor( builder, 3, list(weights_data.shape), tensor_type=_tfl_tensor_type.FLOAT32 ) params_tensor = _build_tensor( builder, 4, list(params_data.shape), tensor_type=_tfl_tensor_type.FLOAT32 ) output_tensor = _build_tensor(builder, 5, output_shape, tensor_type=_tfl_tensor_type.FLOAT32) sparse_op = _build_operator( builder, 0, [0, 1, 2, 3, 4], [5], builtin_options_type=_get_builtin_options_type("EmbeddingLookupSparseOptions"), builtin_options=sparse_options, ) subgraph = _build_subgraph( builder, tensors=[ ids_tensor, indices_tensor, dense_shape_tensor, weights_tensor, params_tensor, output_tensor, ], operators=[sparse_op], inputs=[], outputs=[5], ) operator_codes = [ _build_operator_code(builder, _get_builtin_operator("EMBEDDING_LOOKUP_SPARSE")) ] buffers = [ _build_buffer(builder, ids_data.tobytes()), _build_buffer(builder, indices_data.tobytes()), _build_buffer(builder, dense_shape_data.tobytes()), _build_buffer(builder, weights_data.tobytes()), _build_buffer(builder, params_data.tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=buffers, ) def _build_tflite_hashtable_lookup_model(*, value_shape, value_type=None): """Build a model containing one HASHTABLE_LOOKUP operator.""" builder = flatbuffers.Builder(1024) value_type = _tfl_tensor_type.FLOAT32 if value_type is None else value_type lookup_tensor = _build_tensor(builder, 0, [4], tensor_type=_tfl_tensor_type.INT32) key_tensor = _build_tensor(builder, 1, [3], tensor_type=_tfl_tensor_type.INT32) value_tensor = _build_tensor(builder, 2, value_shape, tensor_type=value_type) output_tensor = _build_tensor(builder, 3, [4, *value_shape[1:]], tensor_type=value_type) hits_tensor = _build_tensor(builder, 4, [4], tensor_type=_tfl_tensor_type.UINT8) hashtable_lookup = _build_operator(builder, 0, [0, 1, 2], [3, 4]) main_subgraph = _build_subgraph( builder, tensors=[lookup_tensor, key_tensor, value_tensor, output_tensor, hits_tensor], operators=[hashtable_lookup], inputs=[0, 1, 2], outputs=[3, 4], ) operator_codes = [_build_operator_code(builder, _get_builtin_operator("HASHTABLE_LOOKUP"))] buffers = [_build_buffer(builder) for _ in range(5)] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=operator_codes, buffers=buffers, ) def test_resource_variable_call_once_init_read(): """Test reading a resource variable initialized by a supported CALL_ONCE subgraph.""" mod = _load_model_from_buffer(_build_tflite_resource_variable_model()) @I.ir_module class Expected: @R.function def main() -> R.Tensor((2,), dtype="float32"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((2,), dtype="float32") = R.const([1.0, 2.0], "float32") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_assign_variable_main_subgraph_unsupported(): """Test ASSIGN_VARIABLE remains unsupported outside CALL_ONCE initialization.""" with pytest.raises(tvm.error.OpNotImplemented, match="ASSIGN_VARIABLE outside CALL_ONCE"): _load_model_from_buffer(_build_tflite_resource_assign_in_main_model()) def test_read_variable_uninitialized_unsupported(): """Test READ_VARIABLE rejects resource handles without supported initialization.""" with pytest.raises(tvm.error.OpNotImplemented, match="READ_VARIABLE requires a resource"): _load_model_from_buffer(_build_tflite_resource_read_uninitialized_model()) def test_hashtable_call_once_import_find_string_to_int64(): """Test HASHTABLE_FIND for a static string-to-int64 table.""" mod = _load_model_from_buffer(_build_tflite_hashtable_find_string_to_int64_model()) @I.ir_module class Expected: @R.function def main() -> R.Tensor((3,), dtype="int64"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((3,), dtype="int64") = R.const([10, -1, 20], "int64") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_hashtable_call_once_import_find_string_to_int64_2d_query(): """Test HASHTABLE_FIND preserves the static query shape.""" mod = _load_model_from_buffer( _build_tflite_hashtable_find_string_to_int64_model( query_values=["alpha", "beta", "missing", "gamma"], query_shape=[2, 2], ) ) @I.ir_module class Expected: @R.function def main() -> R.Tensor((2, 2), dtype="int64"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="int64") = R.const([[10, 20], [-1, 30]], "int64") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_hashtable_call_once_import_find_int64_to_string_unsupported(): """Test HASHTABLE_FIND rejects int64-to-string tables until string outputs exist.""" with pytest.raises(tvm.error.OpNotImplemented, match="string -> int64"): _load_model_from_buffer(_build_tflite_hashtable_find_int64_to_string_model()) def test_hashtable_call_once_import_find_runtime_query_unsupported(): """Test HASHTABLE_FIND rejects runtime string queries.""" with pytest.raises(tvm.error.OpNotImplemented, match="string queries|STRING graph inputs"): _load_model_from_buffer( _build_tflite_hashtable_find_string_to_int64_model(query_is_input=True) ) def test_hashtable_call_once_import_duplicate_keys_unsupported(): """Test HASHTABLE_IMPORT rejects duplicate static keys.""" with pytest.raises(tvm.error.OpNotImplemented, match="duplicate keys"): _load_model_from_buffer( _build_tflite_hashtable_find_string_to_int64_model( table_keys=["alpha", "alpha"], table_values=[10, 20] ) ) def test_hashtable_call_once_import_size(): """Test HASHTABLE_SIZE for a table initialized by a supported CALL_ONCE subgraph.""" mod = _load_model_from_buffer(_build_tflite_hashtable_size_model()) @I.ir_module class Expected: @R.function def main() -> R.Tensor((1,), dtype="int64"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((1,), dtype="int64") = R.const([2], "int64") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_hashtable_import_main_subgraph_unsupported(): """Test HASHTABLE_IMPORT remains unsupported outside CALL_ONCE initialization.""" with pytest.raises(tvm.error.OpNotImplemented, match="HASHTABLE_IMPORT outside CALL_ONCE"): _load_model_from_buffer(_build_tflite_hashtable_import_in_main_model()) def test_hashtable_size_uninitialized_unsupported(): """Test HASHTABLE_SIZE rejects tables without supported initialization.""" with pytest.raises(tvm.error.OpNotImplemented, match="HASHTABLE_SIZE requires a table"): _load_model_from_buffer(_build_tflite_hashtable_size_uninitialized_model()) def test_embedding_lookup_sparse_sum(): from tflite.CombinerType import CombinerType mod = _load_model_from_buffer( _build_tflite_embedding_lookup_sparse_model( CombinerType.SUM, indices_data=[[0, 0], [2, 0], [2, 1]], dense_shape_data=[3, 2], ) ) out = _run_no_input_module(mod) expected = np.array( [ [[1.00, 1.01], [1.10, 1.11], [1.20, 1.21]], [[0.00, 0.00], [0.00, 0.00], [0.00, 0.00]], [[6.00, 6.06], [6.60, 6.66], [7.20, 7.26]], ], dtype=np.float32, ) np.testing.assert_allclose(out, expected, rtol=1e-5, atol=1e-5) def test_embedding_lookup_sparse_mean(): from tflite.CombinerType import CombinerType mod = _load_model_from_buffer( _build_tflite_embedding_lookup_sparse_model( CombinerType.MEAN, indices_data=[[0, 0], [2, 0], [2, 1]], dense_shape_data=[3, 2], ) ) out = _run_no_input_module(mod) expected = np.array( [ [[1.00, 1.01], [1.10, 1.11], [1.20, 1.21]], [[0.00, 0.00], [0.00, 0.00], [0.00, 0.00]], [[1.00, 1.01], [1.10, 1.11], [1.20, 1.21]], ], dtype=np.float32, ) np.testing.assert_allclose(out, expected, rtol=1e-5, atol=1e-5) def test_embedding_lookup_sparse_mean_negative_weights(): from tflite.CombinerType import CombinerType mod = _load_model_from_buffer( _build_tflite_embedding_lookup_sparse_model( CombinerType.MEAN, indices_data=[[0, 0], [0, 1], [2, 0]], dense_shape_data=[3, 2], weights_data=[1.0, -2.0, 0.0], ) ) (output,) = (_run_no_input_module(mod),) expected = np.array( [ [[5.0, 5.01], [5.1, 5.11], [5.2, 5.21]], [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], [[np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan]], ], dtype=np.float32, ) np.testing.assert_allclose(output, expected, rtol=1e-5, atol=1e-5, equal_nan=True) def test_embedding_lookup_sparse_sqrtn(): from tflite.CombinerType import CombinerType mod = _load_model_from_buffer( _build_tflite_embedding_lookup_sparse_model( CombinerType.SQRTN, indices_data=[[0, 0], [2, 0], [2, 1]], dense_shape_data=[3, 2], ) ) out = _run_no_input_module(mod) scale = np.sqrt(20.0).astype("float32") expected = np.array( [ [[1.00, 1.01], [1.10, 1.11], [1.20, 1.21]], [[0.00, 0.00], [0.00, 0.00], [0.00, 0.00]], [ [6.00 / scale, 6.06 / scale], [6.60 / scale, 6.66 / scale], [7.20 / scale, 7.26 / scale], ], ], dtype=np.float32, ) np.testing.assert_allclose(out, expected, rtol=1e-5, atol=1e-5) def test_embedding_lookup_sparse_indices_3d(): from tflite.CombinerType import CombinerType mod = _load_model_from_buffer( _build_tflite_embedding_lookup_sparse_model( CombinerType.SUM, indices_data=[[0, 0, 0], [2, 0, 0], [2, 0, 1]], dense_shape_data=[3, 2, 2], ) ) out = _run_no_input_module(mod) expected = np.zeros((3, 2, 3, 2), dtype=np.float32) expected[0, 0] = np.array([[1.00, 1.01], [1.10, 1.11], [1.20, 1.21]], dtype=np.float32) expected[2, 0] = np.array([[6.00, 6.06], [6.60, 6.66], [7.20, 7.26]], dtype=np.float32) np.testing.assert_allclose(out, expected, rtol=1e-5, atol=1e-5) def test_hashtable_lookup_1d_value(): mod = _load_model_from_buffer(_build_tflite_hashtable_lookup_model(value_shape=[3])) output, hits = _run_module( mod, np.array([1234, -292, -11, 0], dtype=np.int32), np.array([-11, 0, 1234], dtype=np.int32), np.array([0.0, 0.1, 0.4], dtype=np.float32), ) np.testing.assert_allclose(output, np.array([0.4, 0.0, 0.0, 0.1], dtype=np.float32)) np.testing.assert_array_equal(hits, np.array([1, 0, 1, 1], dtype=np.uint8)) def test_hashtable_lookup_2d_value(): mod = _load_model_from_buffer(_build_tflite_hashtable_lookup_model(value_shape=[3, 2])) output, hits = _run_module( mod, np.array([1234, -292, -11, 0], dtype=np.int32), np.array([-11, 0, 1234], dtype=np.int32), np.array([[0.0, 0.1], [1.0, 1.1], [2.0, 2.1]], dtype=np.float32), ) np.testing.assert_allclose( output, np.array( [ [2.0, 2.1], [0.0, 0.0], [0.0, 0.1], [1.0, 1.1], ], dtype=np.float32, ), ) np.testing.assert_array_equal(hits, np.array([1, 0, 1, 1], dtype=np.uint8)) def test_hashtable_lookup_string_value_unsupported(): string_type = _get_string_tensor_type() with pytest.raises(tvm.error.OpNotImplemented, match="STRING graph inputs"): _load_model_from_buffer( _build_tflite_hashtable_lookup_model(value_shape=[3], value_type=string_type) ) def _get_stablehlo_builtin_operator(builtin_name): if not hasattr(_tfl_builtin_operator, builtin_name): pytest.skip(f"TFLite schema does not provide BuiltinOperator.{builtin_name}") return getattr(_tfl_builtin_operator, builtin_name) def _build_stablehlo_model(*, builtin_name, input_count): """Build a minimal TFLite model containing one StableHLO builtin operator.""" builder = flatbuffers.Builder(1024) shape = [2, 2] output_tensor_idx = input_count builtin_op = _get_stablehlo_builtin_operator(builtin_name) tensors = [_build_tensor(builder, buffer_idx, shape) for buffer_idx in range(input_count + 1)] stablehlo_op = _build_operator( builder, 0, list(range(input_count)), [output_tensor_idx], ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[stablehlo_op], inputs=list(range(input_count)), outputs=[output_tensor_idx], ) operator_codes = [_build_operator_code(builder, builtin_op)] buffers = [_build_buffer(builder) for _ in range(input_count + 1)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=buffers ) def _build_stablehlo_model_with_unused_subgraph(): """Build a StableHLO model with an unused extra subgraph.""" builder = flatbuffers.Builder(1024) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_ADD") main_tensors = [_build_tensor(builder, buffer_idx, [2, 2]) for buffer_idx in range(3)] main_op = _build_operator(builder, 0, [0, 1], [2]) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_op], inputs=[0, 1], outputs=[2], ) # Give the unused subgraph a conflicting input tensor name and different # shape. from_tflite should infer the main function input shape only from # Subgraphs(0). extra_tensors = [_build_tensor(builder, buffer_idx, [4, 4]) for buffer_idx in range(3, 6)] extra_op = _build_operator(builder, 0, [0, 1], [2]) extra_subgraph = _build_subgraph( builder, tensors=extra_tensors, operators=[extra_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, builtin_op)] buffers = [_build_buffer(builder) for _ in range(6)] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[extra_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_stablehlo_reduce_model(reducer_name, init_value): """Build a single-input STABLEHLO_REDUCE model with a binary reducer body.""" builder = flatbuffers.Builder(1024) dimensions_vec = _tflite_int64_vector( builder, _tfl_stablehlo_reduce_opts.StablehloReduceOptionsStartDimensionsVector, [1], ) _tfl_stablehlo_reduce_opts.StablehloReduceOptionsStart(builder) _tfl_stablehlo_reduce_opts.StablehloReduceOptionsAddDimensions(builder, dimensions_vec) _tfl_stablehlo_reduce_opts.StablehloReduceOptionsAddBodySubgraphIndex(builder, 1) reduce_opts = _tfl_stablehlo_reduce_opts.StablehloReduceOptionsEnd(builder) reduce_builtin = _get_stablehlo_builtin_operator("STABLEHLO_REDUCE") reducer_builtin = _get_stablehlo_builtin_operator(reducer_name) reduce_code = _build_operator_code(builder, reduce_builtin) reducer_code = _build_operator_code(builder, reducer_builtin) main_tensors = [ _build_tensor(builder, 0, [2, 3]), _build_tensor(builder, 1, []), _build_tensor(builder, 2, [2]), ] reduce_op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloReduceOptions, builtin_options2=reduce_opts, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[reduce_op], inputs=[0], outputs=[2], ) body_tensors = [_build_tensor(builder, buffer_idx, []) for buffer_idx in range(3, 6)] reducer_op = _build_operator(builder, 1, [0, 1], [2]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[reducer_op], inputs=[0, 1], outputs=[2], ) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array(init_value, dtype=np.float32).tobytes()), _build_buffer(builder), _build_buffer(builder), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[body_subgraph], operator_codes=[reduce_code, reducer_code], buffers=buffers, ) def _build_stablehlo_sort_model(comparison_direction, is_stable=False): """Build a single-input STABLEHLO_SORT model with a compare body.""" builder = flatbuffers.Builder(1024) _tfl_stablehlo_sort_opts.StablehloSortOptionsStart(builder) _tfl_stablehlo_sort_opts.StablehloSortOptionsAddDimension(builder, 1) _tfl_stablehlo_sort_opts.StablehloSortOptionsAddIsStable(builder, is_stable) _tfl_stablehlo_sort_opts.StablehloSortOptionsAddComparatorSubgraphIndex(builder, 1) sort_opts = _tfl_stablehlo_sort_opts.StablehloSortOptionsEnd(builder) _tfl_stablehlo_compare_opts.StablehloCompareOptionsStart(builder) _tfl_stablehlo_compare_opts.StablehloCompareOptionsAddComparisonDirection( builder, comparison_direction ) compare_opts = _tfl_stablehlo_compare_opts.StablehloCompareOptionsEnd(builder) sort_builtin = _get_stablehlo_builtin_operator("STABLEHLO_SORT") compare_builtin = _get_stablehlo_builtin_operator("STABLEHLO_COMPARE") sort_code = _build_operator_code(builder, sort_builtin) compare_code = _build_operator_code(builder, compare_builtin) main_tensors = [ _build_tensor(builder, 0, [2, 3]), _build_tensor(builder, 1, [2, 3]), ] sort_op = _build_operator( builder, 0, [0], [1], builtin_options2_type=_tfl_builtin_options2.StablehloSortOptions, builtin_options2=sort_opts, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[sort_op], inputs=[0], outputs=[1], ) body_tensors = [ _build_tensor(builder, 2, []), _build_tensor(builder, 3, []), _build_tensor(builder, 4, [], tensor_type=_tfl_tensor_type.BOOL), ] compare_op = _build_operator( builder, 1, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloCompareOptions, builtin_options2=compare_opts, ) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[compare_op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(5)] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[body_subgraph], operator_codes=[sort_code, compare_code], buffers=buffers, ) def _build_stablehlo_reduce_window_model( reducer_name="STABLEHLO_MAXIMUM", init_value=-np.inf, base_dilations=None, ): """Build an NHWC 2D STABLEHLO_REDUCE_WINDOW model.""" builder = flatbuffers.Builder(1024) if base_dilations is None: base_dilations = [1, 1, 1, 1] window_dimensions_vec = _tflite_int64_vector( builder, _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsStartWindowDimensionsVector, [1, 2, 2, 1], ) window_strides_vec = _tflite_int64_vector( builder, _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsStartWindowStridesVector, [1, 2, 2, 1], ) base_dilations_vec = _tflite_int64_vector( builder, _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsStartBaseDilationsVector, base_dilations, ) window_dilations_vec = _tflite_int64_vector( builder, _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsStartWindowDilationsVector, [1, 1, 1, 1], ) padding_vec = _tflite_int64_vector( builder, _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsStartPaddingVector, [0, 0, 0, 0, 0, 0, 0, 0], ) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsStart(builder) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsAddWindowDimensions( builder, window_dimensions_vec ) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsAddWindowStrides( builder, window_strides_vec ) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsAddBaseDilations( builder, base_dilations_vec ) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsAddWindowDilations( builder, window_dilations_vec ) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsAddPadding(builder, padding_vec) _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsAddBodySubgraphIndex(builder, 1) reduce_window_opts = _tfl_stablehlo_reduce_window_opts.StablehloReduceWindowOptionsEnd(builder) reduce_window_builtin = _get_stablehlo_builtin_operator("STABLEHLO_REDUCE_WINDOW") reducer_builtin = _get_stablehlo_builtin_operator(reducer_name) reduce_window_code = _build_operator_code(builder, reduce_window_builtin) reducer_code = _build_operator_code(builder, reducer_builtin) main_tensors = [ _build_tensor(builder, 0, [1, 4, 4, 1]), _build_tensor(builder, 1, []), _build_tensor(builder, 2, [1, 2, 2, 1]), ] reduce_window_op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloReduceWindowOptions, builtin_options2=reduce_window_opts, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[reduce_window_op], inputs=[0], outputs=[2], ) body_tensors = [_build_tensor(builder, buffer_idx, []) for buffer_idx in range(3, 6)] reducer_op = _build_operator(builder, 1, [0, 1], [2]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[reducer_op], inputs=[0, 1], outputs=[2], ) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array(init_value, dtype=np.float32).tobytes()), _build_buffer(builder), _build_buffer(builder), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[body_subgraph], operator_codes=[reduce_window_code, reducer_code], buffers=buffers, ) def _build_stablehlo_scatter_model(reducer_name="STABLEHLO_ADD", update_window_dims=None): """Build a canonical point-update STABLEHLO_SCATTER model.""" builder = flatbuffers.Builder(1024) if update_window_dims is None: update_window_dims = [] update_window_dims_vec = _tflite_int64_vector( builder, _tfl_stablehlo_scatter_opts.StablehloScatterOptionsStartUpdateWindowDimsVector, update_window_dims, ) inserted_window_dims_vec = _tflite_int64_vector( builder, _tfl_stablehlo_scatter_opts.StablehloScatterOptionsStartInsertedWindowDimsVector, [0], ) scatter_dims_vec = _tflite_int64_vector( builder, _tfl_stablehlo_scatter_opts.StablehloScatterOptionsStartScatterDimsToOperandDimsVector, [0], ) _tfl_stablehlo_scatter_opts.StablehloScatterOptionsStart(builder) _tfl_stablehlo_scatter_opts.StablehloScatterOptionsAddUpdateWindowDims( builder, update_window_dims_vec ) _tfl_stablehlo_scatter_opts.StablehloScatterOptionsAddInsertedWindowDims( builder, inserted_window_dims_vec ) _tfl_stablehlo_scatter_opts.StablehloScatterOptionsAddScatterDimsToOperandDims( builder, scatter_dims_vec ) _tfl_stablehlo_scatter_opts.StablehloScatterOptionsAddIndexVectorDim(builder, 1) _tfl_stablehlo_scatter_opts.StablehloScatterOptionsAddUpdateComputationSubgraphIndex(builder, 1) scatter_opts = _tfl_stablehlo_scatter_opts.StablehloScatterOptionsEnd(builder) scatter_builtin = _get_stablehlo_builtin_operator("STABLEHLO_SCATTER") reducer_builtin = _get_stablehlo_builtin_operator(reducer_name) scatter_code = _build_operator_code(builder, scatter_builtin) reducer_code = _build_operator_code(builder, reducer_builtin) main_tensors = [ _build_tensor(builder, 0, [4]), _build_tensor(builder, 1, [2, 1], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 2, [2]), _build_tensor(builder, 3, [4]), ] scatter_op = _build_operator( builder, 0, [0, 1, 2], [3], builtin_options2_type=_tfl_builtin_options2.StablehloScatterOptions, builtin_options2=scatter_opts, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[scatter_op], inputs=[0, 1, 2], outputs=[3], ) body_tensors = [_build_tensor(builder, buffer_idx, []) for buffer_idx in range(4, 7)] reducer_op = _build_operator(builder, 1, [0, 1], [2]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[reducer_op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(7)] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[body_subgraph], operator_codes=[scatter_code, reducer_code], buffers=buffers, ) def _build_stablehlo_custom_call_model( call_target_name="Sharding", has_side_effect=False, output_tensor_type=_tfl_tensor_type.FLOAT32, include_options=True, ): """Build a single-input STABLEHLO_CUSTOM_CALL model. When ``include_options`` is False the operator declares the StablehloCustomCallOptions type but omits the options table, emulating a malformed flatbuffer with a missing BuiltinOptions2 payload. """ builder = flatbuffers.Builder(1024) custom_call_opts = None if include_options: call_target_name_offset = builder.CreateString(call_target_name) backend_config_offset = builder.CreateString("") _tfl_stablehlo_custom_call_opts.StablehloCustomCallOptionsStart(builder) _tfl_stablehlo_custom_call_opts.StablehloCustomCallOptionsAddCallTargetName( builder, call_target_name_offset ) _tfl_stablehlo_custom_call_opts.StablehloCustomCallOptionsAddHasSideEffect( builder, has_side_effect ) _tfl_stablehlo_custom_call_opts.StablehloCustomCallOptionsAddBackendConfig( builder, backend_config_offset ) custom_call_opts = _tfl_stablehlo_custom_call_opts.StablehloCustomCallOptionsEnd(builder) custom_call_builtin = _get_stablehlo_builtin_operator("STABLEHLO_CUSTOM_CALL") custom_call_code = _build_operator_code(builder, custom_call_builtin) main_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, [2, 2], tensor_type=output_tensor_type), ] custom_call_op = _build_operator( builder, 0, [0], [1], builtin_options2_type=_tfl_builtin_options2.StablehloCustomCallOptions, builtin_options2=custom_call_opts, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[custom_call_op], inputs=[0], outputs=[1], ) buffers = [_build_buffer(builder) for _ in range(2)] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=[custom_call_code], buffers=buffers, ) def _build_stablehlo_while_model( cond_subgraph_index=1, body_subgraph_index=2, cond_output_type=_tfl_tensor_type.BOOL, cond_input_type=_tfl_tensor_type.INT32, body_outputs=None, body_input_type=_tfl_tensor_type.INT32, body_output_type=_tfl_tensor_type.INT32, main_output_type=_tfl_tensor_type.INT32, ): """Build a STABLEHLO_WHILE model incrementing an int32 scalar until i < 3 is false.""" builder = flatbuffers.Builder(1024) body_outputs = [2] if body_outputs is None else body_outputs while_options = _build_stablehlo_while_options( builder, cond_subgraph_index, body_subgraph_index ) _tfl_stablehlo_compare_opts.StablehloCompareOptionsStart(builder) _tfl_stablehlo_compare_opts.StablehloCompareOptionsAddComparisonDirection( builder, _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_LT, ) compare_opts = _tfl_stablehlo_compare_opts.StablehloCompareOptionsEnd(builder) one = np.array(1, dtype=np.int32) three = np.array(3, dtype=np.int32) main_tensors = [ _build_tensor(builder, 0, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=main_output_type), ] main_while = _build_operator( builder, 0, [0], [1], builtin_options2_type=_tfl_builtin_options2.StablehloWhileOptions, builtin_options2=while_options, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[main_while], inputs=[0], outputs=[1], ) cond_tensors = [ _build_tensor(builder, 0, [], tensor_type=cond_input_type), _build_tensor(builder, 1, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=cond_output_type), ] cond_compare = _build_operator( builder, 1, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloCompareOptions, builtin_options2=compare_opts, ) cond_subgraph = _build_subgraph( builder, tensors=cond_tensors, operators=[cond_compare], inputs=[0], outputs=[2], ) body_tensors = [ _build_tensor(builder, 0, [], tensor_type=body_input_type), _build_tensor(builder, 2, [], tensor_type=_tfl_tensor_type.INT32), _build_tensor(builder, 3, [], tensor_type=body_output_type), ] body_add = _build_operator(builder, 2, [0, 1], [2]) body_subgraph = _build_subgraph( builder, tensors=body_tensors, operators=[body_add], inputs=[0], outputs=body_outputs, ) operator_codes = [ _build_operator_code(builder, _get_stablehlo_builtin_operator("STABLEHLO_WHILE")), _build_operator_code(builder, _get_stablehlo_builtin_operator("STABLEHLO_COMPARE")), _build_operator_code(builder, _get_stablehlo_builtin_operator("STABLEHLO_ADD")), ] buffers = [ _build_buffer(builder), _build_buffer(builder, three.tobytes()), _build_buffer(builder, one.tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[cond_subgraph, body_subgraph], operator_codes=operator_codes, buffers=buffers, ) def _build_stablehlo_composite_model(with_attributes=False, use_main_input_after_composite=False): """Build a STABLEHLO_COMPOSITE model that decomposes to STABLEHLO_NEGATE.""" builder = flatbuffers.Builder(1024) name = builder.CreateString("test.negate") attributes = None if with_attributes: _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsStartCompositeAttributesVector( builder, 1 ) builder.PrependUint8(1) attributes = builder.EndVector() _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsStart(builder) _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsAddName(builder, name) _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsAddVersion(builder, 1) _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsAddDecompositionSubgraphIndex(builder, 1) if attributes is not None: _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsAddCompositeAttributes( builder, attributes ) composite_opts = _tfl_stablehlo_composite_opts.StableHLOCompositeOptionsEnd(builder) composite_builtin = _get_stablehlo_builtin_operator("STABLEHLO_COMPOSITE") negate_builtin = _get_stablehlo_builtin_operator("STABLEHLO_NEGATE") add_builtin = _get_stablehlo_builtin_operator("STABLEHLO_ADD") composite_code = _build_operator_code(builder, composite_builtin) negate_code = _build_operator_code(builder, negate_builtin) add_code = _build_operator_code(builder, add_builtin) main_tensors = [ _build_tensor(builder, 0, [2, 2]), _build_tensor(builder, 1, [2, 2]), _build_tensor(builder, 2, [2, 2]), ] composite_op = _build_operator( builder, 0, [0], [1], builtin_options2_type=_tfl_builtin_options2.StableHLOCompositeOptions, builtin_options2=composite_opts, ) main_ops = [composite_op] main_outputs = [1] if use_main_input_after_composite: main_ops.append(_build_operator(builder, 2, [0, 1], [2])) main_outputs = [2] main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=main_ops, inputs=[0], outputs=main_outputs, ) decomposition_tensors = [ _build_tensor(builder, 2, [2, 2]), _build_tensor(builder, 3, [2, 2]), ] negate_op = _build_operator(builder, 1, [0], [1]) decomposition_subgraph = _build_subgraph( builder, tensors=decomposition_tensors, operators=[negate_op], inputs=[0], outputs=[1], ) buffers = [_build_buffer(builder) for _ in range(4)] return _finish_tflite_model( builder, subgraph=main_subgraph, extra_subgraphs=[decomposition_subgraph], operator_codes=[composite_code, negate_code, add_code], buffers=buffers, ) def _build_stablehlo_typed_binary_model(*, builtin_name, tensor_type): """Build a minimal TFLite StableHLO binary model with the requested tensor type.""" builder = flatbuffers.Builder(1024) shape = [2, 2] output_tensor_idx = 2 builtin_op = _get_stablehlo_builtin_operator(builtin_name) tensors = [ _build_tensor(builder, buffer_idx, shape, tensor_type=tensor_type) for buffer_idx in range(3) ] stablehlo_op = _build_operator(builder, 0, [0, 1], [output_tensor_idx]) subgraph = _build_subgraph( builder, tensors=tensors, operators=[stablehlo_op], inputs=[0, 1], outputs=[output_tensor_idx], ) operator_codes = [_build_operator_code(builder, builtin_op)] buffers = [_build_buffer(builder) for _ in range(3)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=buffers ) @pytest.mark.parametrize( "builtin_name, relax_op", [ ("STABLEHLO_ABS", R.abs), ("STABLEHLO_COSINE", R.cos), ("STABLEHLO_EXPONENTIAL", R.exp), ("STABLEHLO_FLOOR", R.floor), ("STABLEHLO_LOG", R.log), ("STABLEHLO_LOGISTIC", R.sigmoid), ("STABLEHLO_NEGATE", R.negative), ("STABLEHLO_RSQRT", R.rsqrt), ("STABLEHLO_TANH", R.tanh), ], ) def test_stablehlo_unary(builtin_name, relax_op): """TFLite StableHLO unary elementwise operators.""" mod = _load_model_from_buffer(_build_stablehlo_model(builtin_name=builtin_name, input_count=1)) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = relax_op(x) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) @pytest.mark.parametrize( "builtin_name, relax_op", [ ("STABLEHLO_ADD", R.add), ("STABLEHLO_DIVIDE", R.divide), ("STABLEHLO_MAXIMUM", R.maximum), ("STABLEHLO_MINIMUM", R.minimum), ("STABLEHLO_MULTIPLY", R.multiply), ("STABLEHLO_POWER", R.power), ("STABLEHLO_SUBTRACT", R.subtract), ], ) def test_stablehlo_binary(builtin_name, relax_op): """TFLite StableHLO binary elementwise operators.""" mod = _load_model_from_buffer(_build_stablehlo_model(builtin_name=builtin_name, input_count=2)) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = relax_op(x, y) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_model_with_unused_subgraph(): """TFLite StableHLO import ignores unused non-main subgraphs.""" mod = _load_model_from_buffer(_build_stablehlo_model_with_unused_subgraph()) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add(x, y) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) @pytest.mark.parametrize( "reducer_name, init_value, relax_op", [ ("STABLEHLO_ADD", 0.0, R.sum), ("STABLEHLO_MAXIMUM", -np.inf, R.max), ("STABLEHLO_MINIMUM", np.inf, R.min), ("STABLEHLO_MULTIPLY", 1.0, R.prod), ], ) def test_stablehlo_reduce(reducer_name, init_value, relax_op): """TFLite StableHLO REDUCE with simple binary reducer body subgraphs.""" mod = _load_model_from_buffer(_build_stablehlo_reduce_model(reducer_name, init_value)) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2,), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2,), dtype="float32") = relax_op(x, axis=[1], keepdims=False) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_reduce_unsupported_reducer(): """TFLite StableHLO REDUCE rejects unsupported body reducer ops.""" buf = _build_stablehlo_reduce_model("STABLEHLO_SUBTRACT", 0.0) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="reducer"): from_tflite(tflite_model) def test_stablehlo_reduce_non_identity_init_unsupported(): """TFLite StableHLO REDUCE rejects init values that Relax reductions cannot express.""" buf = _build_stablehlo_reduce_model("STABLEHLO_ADD", 1.0) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="init value"): from_tflite(tflite_model) @pytest.mark.parametrize( "comparison_direction, descending", [ ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_LT, False, ), ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_GT, True, ), ], ) def test_stablehlo_sort(comparison_direction, descending): """TFLite StableHLO SORT with LT/GT scalar compare body subgraphs.""" mod = _load_model_from_buffer(_build_stablehlo_sort_model(comparison_direction)) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.sort(x, axis=1, descending=descending) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_sort_unsupported_comparator(): """TFLite StableHLO SORT rejects non-ordering comparators.""" _DIR = _tfl_stablehlo_comp_dir.StablehloComparisonDirection buf = _build_stablehlo_sort_model(_DIR.STABLEHLO_COMPARISON_DIRECTION_EQ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="LT or GT"): from_tflite(tflite_model) def test_stablehlo_sort_stable_unsupported(): """TFLite StableHLO SORT rejects stable sort until Relax exposes that contract.""" _DIR = _tfl_stablehlo_comp_dir.StablehloComparisonDirection buf = _build_stablehlo_sort_model(_DIR.STABLEHLO_COMPARISON_DIRECTION_LT, is_stable=True) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="stable sort"): from_tflite(tflite_model) def test_stablehlo_reduce_window_max_pool2d(): """TFLite StableHLO REDUCE_WINDOW max reducer lowers to NHWC max_pool2d.""" mod = _load_model_from_buffer(_build_stablehlo_reduce_window_model()) @I.ir_module class Expected: @R.function def main( x: R.Tensor((1, 4, 4, 1), dtype="float32"), ) -> R.Tensor((1, 2, 2, 1), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 2, 2, 1), dtype="float32") = R.nn.max_pool2d( x, pool_size=[2, 2], strides=[2, 2], padding=[0, 0, 0, 0], dilation=[1, 1], ceil_mode=False, layout="NHWC", out_layout="NHWC", ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_reduce_window_unsupported_reducer(): """TFLite StableHLO REDUCE_WINDOW rejects non-max reducers in the pool subset.""" buf = _build_stablehlo_reduce_window_model(reducer_name="STABLEHLO_ADD", init_value=0.0) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="MAXIMUM"): from_tflite(tflite_model) def test_stablehlo_reduce_window_base_dilation_unsupported(): """TFLite StableHLO REDUCE_WINDOW rejects base dilation in the pool subset.""" buf = _build_stablehlo_reduce_window_model(base_dilations=[1, 2, 1, 1]) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="base dilation"): from_tflite(tflite_model) @pytest.mark.parametrize( "reducer_name, reduction", [ ("STABLEHLO_ADD", "add"), ("STABLEHLO_MAXIMUM", "max"), ("STABLEHLO_MINIMUM", "min"), ("STABLEHLO_MULTIPLY", "mul"), ], ) def test_stablehlo_scatter(reducer_name, reduction): """TFLite StableHLO SCATTER point updates lower to Relax scatter_nd.""" mod = _load_model_from_buffer(_build_stablehlo_scatter_model(reducer_name)) @I.ir_module class Expected: @R.function def main( operand: R.Tensor((4,), dtype="float32"), indices: R.Tensor((2, 1), dtype="int32"), updates: R.Tensor((2,), dtype="float32"), ) -> R.Tensor((4,), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv: R.Tensor((4,), dtype="float32") = R.scatter_nd( operand, indices, updates, reduction=reduction ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_scatter_unsupported_reducer(): """TFLite StableHLO SCATTER rejects unsupported update computation ops.""" buf = _build_stablehlo_scatter_model(reducer_name="STABLEHLO_SUBTRACT") if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="reducer"): from_tflite(tflite_model) def test_stablehlo_scatter_update_window_unsupported(): """TFLite StableHLO SCATTER rejects slice update windows in the point subset.""" buf = _build_stablehlo_scatter_model(update_window_dims=[0]) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="point updates"): from_tflite(tflite_model) def test_stablehlo_custom_call_sharding(): """TFLite StableHLO CUSTOM_CALL Sharding annotation lowers to identity.""" mod = _load_model_from_buffer(_build_stablehlo_custom_call_model()) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = x R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_custom_call_unsupported_target(): """TFLite StableHLO CUSTOM_CALL rejects unknown external call targets.""" buf = _build_stablehlo_custom_call_model(call_target_name="custom_backend") with pytest.raises( tvm.error.OpNotImplemented, match="STABLEHLO_CUSTOM_CALL target custom_backend is not supported", ): _load_model_from_buffer(buf) def test_stablehlo_custom_call_sharding_side_effect_unsupported(): """TFLite StableHLO CUSTOM_CALL rejects side-effecting Sharding calls.""" buf = _build_stablehlo_custom_call_model(has_side_effect=True) with pytest.raises(tvm.error.OpNotImplemented, match="side effects"): _load_model_from_buffer(buf) def test_stablehlo_custom_call_sharding_metadata_mismatch_unsupported(): """TFLite StableHLO CUSTOM_CALL rejects Sharding calls that change tensor metadata.""" buf = _build_stablehlo_custom_call_model(output_tensor_type=_tfl_tensor_type.INT32) with pytest.raises(tvm.error.OpNotImplemented, match="Sharding tensor metadata mismatch"): _load_model_from_buffer(buf) def test_stablehlo_options_missing_payload_unsupported(): """A StableHLO op that declares an options type but omits the payload fails cleanly.""" buf = _build_stablehlo_custom_call_model(include_options=False) with pytest.raises( tvm.error.OpNotImplemented, match="StablehloCustomCallOptions is required but missing from the operator", ): _load_model_from_buffer(buf) def _build_stablehlo_rng_model(algorithm, state_len, out_shape, out_tensor_type, const_state=None): """Build a STABLEHLO_RNG_BIT_GENERATOR model. When ``const_state`` is provided, the uint64 initial state is embedded as a constant tensor (no graph input); otherwise it is a graph input. """ builder = flatbuffers.Builder(1024) _tfl_stablehlo_rng_opts.StablehloRngBitGeneratorOptionsStart(builder) _tfl_stablehlo_rng_opts.StablehloRngBitGeneratorOptionsAddAlgorithm(builder, algorithm) rng_opts = _tfl_stablehlo_rng_opts.StablehloRngBitGeneratorOptionsEnd(builder) rng_builtin = _get_stablehlo_builtin_operator("STABLEHLO_RNG_BIT_GENERATOR") rng_code = _build_operator_code(builder, rng_builtin) main_tensors = [ _build_tensor(builder, 0, [state_len], tensor_type=_tfl_tensor_type.UINT64), _build_tensor(builder, 1, [state_len], tensor_type=_tfl_tensor_type.UINT64), _build_tensor(builder, 2, list(out_shape), tensor_type=out_tensor_type), ] rng_op = _build_operator( builder, 0, [0], [1, 2], builtin_options2_type=_tfl_builtin_options2.StablehloRngBitGeneratorOptions, builtin_options2=rng_opts, ) main_subgraph = _build_subgraph( builder, tensors=main_tensors, operators=[rng_op], inputs=[] if const_state is not None else [0], outputs=[1, 2], ) state_data = None if const_state is not None: state_data = np.array(const_state, dtype="uint64").tobytes() buffers = [ _build_buffer(builder, data=state_data), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=main_subgraph, operator_codes=[rng_code], buffers=buffers, ) _TFL_TENSOR_TYPE_TO_DTYPE = { _tfl_tensor_type.INT32: "int32", _tfl_tensor_type.UINT32: "uint32", _tfl_tensor_type.INT64: "int64", _tfl_tensor_type.UINT64: "uint64", } # Expected vectors are taken verbatim from the TFLite runtime kernel test # (tensorflow/lite/kernels/rng_bit_generator_test.cc), guaranteeing bit-exact parity. _RNG_THREEFRY_EXPECTED = { "int32": [43444564, -2144348869, -315321645, -549236733, 1672743891, -54463903], "uint32": [43444564, 2150618427, 3979645651, 3745730563, 1672743891, 4240503393], "int64": [ -9209908263526143660, -2358953802017238317, -233920680524772397, 2658481902456610144, -2022031683723149139, -2324041912354448873, ], "uint64": [ 9236835810183407956, 16087790271692313299, 18212823393184779219, 2658481902456610144, 16424712389986402477, 16122702161355102743, ], } _RNG_THREEFRY_STATE = {"int32": [1, 5], "uint32": [1, 5], "int64": [1, 8], "uint64": [1, 8]} _RNG_PHILOX_EXPECTED = { "int32": [-263854262, 1366700262, 495645701, -1243243882, 89414891, 1917262711], "uint32": [4031113034, 1366700262, 495645701, 3051723414, 89414891, 1917262711], "int64": [ 5869932932755744586, -5339691813646437371, 8234580641674714347, 2641225993340350124, 1962472297844690804, -3580856229565614135, ], "uint64": [ 5869932932755744586, 13107052260063114245, 8234580641674714347, 2641225993340350124, 1962472297844690804, 14865887844143937481, ], } _RNG_PHILOX_STATE = { "int32": [1, 4, 3], "uint32": [1, 4, 3], "int64": [1, 5, 3], "uint64": [1, 5, 3], } @pytest.mark.parametrize( "out_dtype,out_tensor_type", [ ("int32", _tfl_tensor_type.INT32), ("uint32", _tfl_tensor_type.UINT32), ("int64", _tfl_tensor_type.INT64), ("uint64", _tfl_tensor_type.UINT64), ], ) def test_stablehlo_rng_bit_generator_threefry(out_dtype, out_tensor_type): """TFLite STABLEHLO_RNG_BIT_GENERATOR THREEFRY matches the runtime kernel bit-exactly.""" buf = _build_stablehlo_rng_model(_tfl_rng_algorithm.THREEFRY, 2, [2, 3], out_tensor_type) mod = _load_model_from_buffer(buf) ex = tvm.compile(mod, tvm.target.Target("llvm")) vm = relax.VirtualMachine(ex, tvm.cpu()) result = vm["main"](tvm.runtime.tensor(np.array([1, 2], dtype="uint64"))) state, output = result[0].numpy(), result[1].numpy() assert output.flatten().tolist() == _RNG_THREEFRY_EXPECTED[out_dtype] assert state.tolist() == _RNG_THREEFRY_STATE[out_dtype] @pytest.mark.parametrize( "out_dtype,out_tensor_type", [ ("int32", _tfl_tensor_type.INT32), ("uint32", _tfl_tensor_type.UINT32), ("int64", _tfl_tensor_type.INT64), ("uint64", _tfl_tensor_type.UINT64), ], ) def test_stablehlo_rng_bit_generator_philox(out_dtype, out_tensor_type): """TFLite STABLEHLO_RNG_BIT_GENERATOR PHILOX matches the runtime kernel bit-exactly.""" buf = _build_stablehlo_rng_model(_tfl_rng_algorithm.PHILOX, 3, [2, 3], out_tensor_type) mod = _load_model_from_buffer(buf) ex = tvm.compile(mod, tvm.target.Target("llvm")) vm = relax.VirtualMachine(ex, tvm.cpu()) result = vm["main"](tvm.runtime.tensor(np.array([1, 2, 3], dtype="uint64"))) state, output = result[0].numpy(), result[1].numpy() assert output.flatten().tolist() == _RNG_PHILOX_EXPECTED[out_dtype] assert state.tolist() == _RNG_PHILOX_STATE[out_dtype] def test_stablehlo_rng_bit_generator_default_matches_philox(): """TFLite STABLEHLO_RNG_BIT_GENERATOR DEFAULT resolves to the PHILOX algorithm.""" buf = _build_stablehlo_rng_model(_tfl_rng_algorithm.DEFAULT, 3, [2, 3], _tfl_tensor_type.INT32) mod = _load_model_from_buffer(buf) ex = tvm.compile(mod, tvm.target.Target("llvm")) vm = relax.VirtualMachine(ex, tvm.cpu()) result = vm["main"](tvm.runtime.tensor(np.array([1, 2, 3], dtype="uint64"))) state, output = result[0].numpy(), result[1].numpy() assert output.flatten().tolist() == _RNG_PHILOX_EXPECTED["int32"] assert state.tolist() == _RNG_PHILOX_STATE["int32"] def test_stablehlo_rng_bit_generator_deterministic(): """Re-running the imported RNG kernel yields identical bit-exact output.""" buf = _build_stablehlo_rng_model(_tfl_rng_algorithm.PHILOX, 3, [3, 3], _tfl_tensor_type.INT32) mod = _load_model_from_buffer(buf) ex = tvm.compile(mod, tvm.target.Target("llvm")) vm = relax.VirtualMachine(ex, tvm.cpu()) init = tvm.runtime.tensor(np.array([7, 8, 9], dtype="uint64")) first = vm["main"](init) second = vm["main"](init) np.testing.assert_equal(first[1].numpy(), second[1].numpy()) np.testing.assert_equal(first[0].numpy(), second[0].numpy()) def test_stablehlo_rng_bit_generator_constant_state(): """A constant uint64 initial state imports and stays bit-exact (no graph input).""" buf = _build_stablehlo_rng_model( _tfl_rng_algorithm.THREEFRY, 2, [2, 3], _tfl_tensor_type.INT32, const_state=[1, 2] ) mod = _load_model_from_buffer(buf) assert len(mod["main"].params) == 0 ex = tvm.compile(mod, tvm.target.Target("llvm")) vm = relax.VirtualMachine(ex, tvm.cpu()) result = vm["main"]() assert result[1].numpy().flatten().tolist() == _RNG_THREEFRY_EXPECTED["int32"] assert result[0].numpy().tolist() == _RNG_THREEFRY_STATE["int32"] def test_stablehlo_rng_bit_generator_unsupported_output_dtype(): """TFLite STABLEHLO_RNG_BIT_GENERATOR rejects non-integer output dtypes.""" buf = _build_stablehlo_rng_model(_tfl_rng_algorithm.PHILOX, 3, [2, 3], _tfl_tensor_type.FLOAT32) with pytest.raises(tvm.error.OpNotImplemented, match="output dtype float32 is not supported"): _load_model_from_buffer(buf) def test_stablehlo_rng_bit_generator_threefry_invalid_state_unsupported(): """TFLite STABLEHLO_RNG_BIT_GENERATOR rejects a u64[3] state for THREEFRY.""" buf = _build_stablehlo_rng_model(_tfl_rng_algorithm.THREEFRY, 3, [2, 3], _tfl_tensor_type.INT32) with pytest.raises(tvm.error.OpNotImplemented, match="THREEFRY requires a u64.2. state"): _load_model_from_buffer(buf) def test_stablehlo_rng_bit_generator_non_uint64_state_unsupported(): """TFLite STABLEHLO_RNG_BIT_GENERATOR rejects a non-uint64 initial state.""" builder = flatbuffers.Builder(1024) _tfl_stablehlo_rng_opts.StablehloRngBitGeneratorOptionsStart(builder) _tfl_stablehlo_rng_opts.StablehloRngBitGeneratorOptionsAddAlgorithm( builder, _tfl_rng_algorithm.PHILOX ) rng_opts = _tfl_stablehlo_rng_opts.StablehloRngBitGeneratorOptionsEnd(builder) rng_code = _build_operator_code( builder, _get_stablehlo_builtin_operator("STABLEHLO_RNG_BIT_GENERATOR") ) tensors = [ _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.INT64), _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT64), _build_tensor(builder, 2, [2, 3], tensor_type=_tfl_tensor_type.INT32), ] rng_op = _build_operator( builder, 0, [0], [1, 2], builtin_options2_type=_tfl_builtin_options2.StablehloRngBitGeneratorOptions, builtin_options2=rng_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[rng_op], inputs=[0], outputs=[1, 2] ) buffers = [_build_buffer(builder) for _ in range(3)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[rng_code], buffers=buffers ) with pytest.raises(tvm.error.OpNotImplemented, match="requires a uint64 initial state"): _load_model_from_buffer(buf) def test_stablehlo_while(): """TFLite STABLEHLO_WHILE lowers to a recursive Relax private function.""" mod = _load_model_from_buffer(_build_stablehlo_while_model()) @I.ir_module class Expected: @R.function(private=True) def tflite_stablehlo_while_cond_subgraph_1( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="bool"): with R.dataflow(): gv: R.Tensor((), dtype="bool") = R.less(tvmgen_tensor_0, R.const(3, "int32")) R.output(gv) return gv @R.function(private=True) def tflite_stablehlo_while_body_subgraph_2( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="int32"): with R.dataflow(): gv: R.Tensor((), dtype="int32") = R.add(tvmgen_tensor_0, R.const(1, "int32")) R.output(gv) return gv @R.function(private=True) def tflite_stablehlo_while_subgraph_1_2( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="int32"): cls = Expected while_cond: R.Tensor((), dtype="bool") = cls.tflite_stablehlo_while_cond_subgraph_1( tvmgen_tensor_0 ) if while_cond: gv: R.Tensor((), dtype="int32") = cls.tflite_stablehlo_while_body_subgraph_2( tvmgen_tensor_0 ) gv1: R.Tensor((), dtype="int32") = cls.tflite_stablehlo_while_subgraph_1_2(gv) cond_result: R.Tensor((), dtype="int32") = gv1 else: cond_result: R.Tensor((), dtype="int32") = tvmgen_tensor_0 return cond_result @R.function def main( tvmgen_tensor_0: R.Tensor((), dtype="int32"), ) -> R.Tensor((), dtype="int32"): R.func_attr({"num_input": 1}) cls = Expected with R.dataflow(): gv: R.Tensor((), dtype="int32") = cls.tflite_stablehlo_while_subgraph_1_2( tvmgen_tensor_0 ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_while_non_bool_condition_unsupported(): """STABLEHLO_WHILE rejects cond subgraphs that do not return scalar bool.""" with pytest.raises( tvm.error.OpNotImplemented, match="STABLEHLO_WHILE requires a scalar bool condition" ): _load_model_from_buffer( _build_stablehlo_while_model(cond_output_type=_tfl_tensor_type.INT32) ) def test_stablehlo_while_invalid_index_unsupported(): """STABLEHLO_WHILE rejects invalid cond/body subgraph indices before lowering.""" with pytest.raises( tvm.error.OpNotImplemented, match="STABLEHLO_WHILE requires a valid subgraph index" ): _load_model_from_buffer(_build_stablehlo_while_model(cond_subgraph_index=3)) def test_stablehlo_while_output_count_mismatch_unsupported(): """STABLEHLO_WHILE rejects body subgraphs whose output arity does not match loop vars.""" with pytest.raises( tvm.error.OpNotImplemented, match="STABLEHLO_WHILE subgraph output count mismatch" ): _load_model_from_buffer(_build_stablehlo_while_model(body_outputs=[])) def test_stablehlo_while_input_metadata_mismatch_unsupported(): """STABLEHLO_WHILE rejects cond subgraph inputs whose metadata does not match loop vars.""" with pytest.raises( tvm.error.OpNotImplemented, match="STABLEHLO_WHILE subgraph input tensor metadata mismatch", ): _load_model_from_buffer( _build_stablehlo_while_model(cond_input_type=_tfl_tensor_type.FLOAT32) ) def test_stablehlo_while_output_metadata_mismatch_unsupported(): """STABLEHLO_WHILE rejects body outputs whose metadata does not match loop vars.""" with pytest.raises( tvm.error.OpNotImplemented, match="STABLEHLO_WHILE subgraph output tensor metadata mismatch", ): _load_model_from_buffer( _build_stablehlo_while_model(body_output_type=_tfl_tensor_type.FLOAT32) ) def test_stablehlo_composite(): """TFLite StableHLO COMPOSITE inlines a simple decomposition subgraph.""" mod = _load_model_from_buffer(_build_stablehlo_composite_model()) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.negative(x) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_composite_does_not_overwrite_main_bindings(): """TFLite StableHLO COMPOSITE decomposition tensor names are scoped locally.""" mod = _load_model_from_buffer( _build_stablehlo_composite_model(use_main_input_after_composite=True) ) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.negative(x) gv: R.Tensor((2, 2), dtype="float32") = R.add(x, lv) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_composite_attributes_unsupported(): """TFLite StableHLO COMPOSITE rejects attributes until they are parsed.""" buf = _build_stablehlo_composite_model(with_attributes=True) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="composite attributes"): from_tflite(tflite_model) @pytest.mark.parametrize( "builtin_name, relax_op, dtype, tensor_type", [ ("STABLEHLO_AND", R.logical_and, "bool", _tfl_tensor_type.BOOL), ("STABLEHLO_OR", R.logical_or, "bool", _tfl_tensor_type.BOOL), ("STABLEHLO_AND", R.bitwise_and, "int32", _tfl_tensor_type.INT32), ("STABLEHLO_OR", R.bitwise_or, "int32", _tfl_tensor_type.INT32), ("STABLEHLO_SHIFT_LEFT", R.left_shift, "int32", _tfl_tensor_type.INT32), ], ) def test_stablehlo_typed_binary(builtin_name, relax_op, dtype, tensor_type): """TFLite StableHLO binary elementwise operators with non-float dtype requirements.""" mod = _load_model_from_buffer( _build_stablehlo_typed_binary_model(builtin_name=builtin_name, tensor_type=tensor_type) ) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype=dtype), y: R.Tensor((2, 2), dtype=dtype), ) -> R.Tensor((2, 2), dtype=dtype): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 2), dtype=dtype) = relax_op(x, y) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) @pytest.mark.parametrize( "builtin_name, relax_op", [ ("STABLEHLO_SELECT", R.where), ], ) def test_stablehlo_ternary(builtin_name, relax_op): """TFLite StableHLO ternary elementwise operators.""" builder = flatbuffers.Builder(1024) shape = [2, 2] builtin_op = _get_stablehlo_builtin_operator(builtin_name) # First input (condition) must be bool for R.where tensor_0 = _build_tensor(builder, 0, shape, tensor_type=_tfl_tensor_type.BOOL) tensor_1 = _build_tensor(builder, 1, shape) tensor_2 = _build_tensor(builder, 2, shape) tensor_out = _build_tensor(builder, 3, shape) tensors = [tensor_0, tensor_1, tensor_2, tensor_out] stablehlo_op = _build_operator( builder, 0, [0, 1, 2], [3], ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[stablehlo_op], inputs=[0, 1, 2], outputs=[3], ) operator_codes = [_build_operator_code(builder, builtin_op)] buffers = [_build_buffer(builder) for _ in range(4)] mod = _load_model_from_buffer( _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=buffers ) ) @I.ir_module class Expected: @R.function def main( c: R.Tensor((2, 2), dtype="bool"), x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = relax_op(c, x, y) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_stablehlo_convert_model(): """STABLEHLO_CONVERT: float32 input -> int32 output.""" builder = flatbuffers.Builder(1024) shape = [2, 2] t_in = _build_tensor(builder, 0, shape, tensor_type=_tfl_tensor_type.FLOAT32) t_out = _build_tensor(builder, 1, shape, tensor_type=_tfl_tensor_type.INT32) tensors = [t_in, t_out] op_code = _build_operator_code(builder, _get_stablehlo_builtin_operator("STABLEHLO_CONVERT")) op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0], outputs=[1], ) buffers = [_build_buffer(builder) for _ in range(2)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_convert(): """TFLite StableHLO CONVERT (astype float32 -> int32).""" mod = _load_model_from_buffer(_build_stablehlo_convert_model()) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="int32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="int32") = R.astype(x, dtype="int32") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_clamp(): """TFLite StableHLO CLAMP (clip with min/operand/max order).""" mod = _load_model_from_buffer( _build_stablehlo_model(builtin_name="STABLEHLO_CLAMP", input_count=3) ) @I.ir_module class Expected: @R.function def main( m: R.Tensor((2, 2), dtype="float32"), x: R.Tensor((2, 2), dtype="float32"), M: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.minimum(R.maximum(x, m), M) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_stablehlo_concat_model(dimension, num_inputs): """STABLEHLO_CONCATENATE with given dimension and number of inputs.""" builder = flatbuffers.Builder(1024) shape = [2, 2] # Build concat options _tfl_stablehlo_concat_opts.StablehloConcatenateOptionsStart(builder) _tfl_stablehlo_concat_opts.StablehloConcatenateOptionsAddDimension(builder, dimension) concat_opts = _tfl_stablehlo_concat_opts.StablehloConcatenateOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_CONCATENATE") op_code = _build_operator_code(builder, builtin_op) if dimension == 0: out_shape = [num_inputs * shape[0], shape[1]] else: out_shape = [shape[0], num_inputs * shape[1]] tensors = [_build_tensor(builder, i, shape) for i in range(num_inputs)] + [ _build_tensor(builder, num_inputs, out_shape) ] op = _build_operator( builder, 0, list(range(num_inputs)), [num_inputs], builtin_options2_type=_tfl_builtin_options2.StablehloConcatenateOptions, builtin_options2=concat_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=list(range(num_inputs)), outputs=[num_inputs], ) buffers = [_build_buffer(builder) for _ in range(num_inputs + 1)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) @pytest.mark.parametrize("dimension", [0, 1]) def test_stablehlo_concatenate(dimension): """TFLite StableHLO CONCATENATE with 2 inputs along given axis.""" num_inputs = 2 mod = _load_model_from_buffer( _build_stablehlo_concat_model(dimension=dimension, num_inputs=num_inputs) ) out_dim = (4, 2) if dimension == 0 else (2, 4) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor(out_dim, dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor(out_dim, dtype="float32") = R.concat((x, y), axis=dimension) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_stablehlo_broadcast_in_dim_model(input_shape, broadcast_dims, output_shape): """STABLEHLO_BROADCAST_IN_DIM with given broadcast dimensions.""" builder = flatbuffers.Builder(1024) # Build broadcast dimensions vector _tfl_stablehlo_bcast_opts.StablehloBroadcastInDimOptionsStartBroadcastDimensionsVector( builder, len(broadcast_dims) ) for d in reversed(broadcast_dims): builder.PrependInt64(d) dims_vec = builder.EndVector() _tfl_stablehlo_bcast_opts.StablehloBroadcastInDimOptionsStart(builder) _tfl_stablehlo_bcast_opts.StablehloBroadcastInDimOptionsAddBroadcastDimensions( builder, dims_vec ) bcast_opts = _tfl_stablehlo_bcast_opts.StablehloBroadcastInDimOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_BROADCAST_IN_DIM") op_code = _build_operator_code(builder, builtin_op) t_in = _build_tensor(builder, 0, input_shape) t_out = _build_tensor(builder, 1, output_shape) tensors = [t_in, t_out] op = _build_operator( builder, 0, [0], [1], builtin_options2_type=_tfl_builtin_options2.StablehloBroadcastInDimOptions, builtin_options2=bcast_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0], outputs=[1], ) buffers = [_build_buffer(builder) for _ in range(2)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_broadcast_in_dim(): """TFLite StableHLO BROADCAST_IN_DIM: (3,) -> (2, 3) with dims=[1].""" mod = _load_model_from_buffer( _build_stablehlo_broadcast_in_dim_model( input_shape=[3], broadcast_dims=[1], output_shape=[2, 3] ) ) @I.ir_module class Expected: @R.function def main(x: R.Tensor((3,), dtype="float32")) -> R.Tensor((2, 3), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="float32") = R.broadcast_to(R.reshape(x, (1, 3)), (2, 3)) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_stablehlo_iota_model(iota_dimension, output_shape): """STABLEHLO_IOTA with given iota dimension and output shape.""" builder = flatbuffers.Builder(1024) _tfl_stablehlo_iota_opts.StablehloIotaOptionsStart(builder) _tfl_stablehlo_iota_opts.StablehloIotaOptionsAddIotaDimension(builder, iota_dimension) iota_opts = _tfl_stablehlo_iota_opts.StablehloIotaOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_IOTA") op_code = _build_operator_code(builder, builtin_op) t_out = _build_tensor(builder, 0, output_shape, tensor_type=_tfl_tensor_type.INT32) tensors = [t_out] op = _build_operator( builder, 0, [], [0], builtin_options2_type=_tfl_builtin_options2.StablehloIotaOptions, builtin_options2=iota_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[], outputs=[0], ) buffers = [_build_buffer(builder)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_iota(): """TFLite StableHLO IOTA: iota_dim=1, shape=(2, 3), dtype=int32.""" mod = _load_model_from_buffer( _build_stablehlo_iota_model(iota_dimension=1, output_shape=[2, 3]) ) @I.ir_module class Expected: @R.function def main() -> R.Tensor((2, 3), dtype="int32"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((2, 3), dtype="int32") = R.broadcast_to( R.reshape(R.arange(0, 3, 1, dtype="int32"), (1, 3)), (2, 3) ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_stablehlo_compare_model(direction): """STABLEHLO_COMPARE with given comparison direction.""" builder = flatbuffers.Builder(1024) _tfl_stablehlo_compare_opts.StablehloCompareOptionsStart(builder) _tfl_stablehlo_compare_opts.StablehloCompareOptionsAddComparisonDirection(builder, direction) cmp_opts = _tfl_stablehlo_compare_opts.StablehloCompareOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_COMPARE") op_code = _build_operator_code(builder, builtin_op) shape = [2, 2] t_lhs = _build_tensor(builder, 0, shape) t_rhs = _build_tensor(builder, 1, shape) t_out = _build_tensor(builder, 2, shape, tensor_type=_tfl_tensor_type.BOOL) tensors = [t_lhs, t_rhs, t_out] op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloCompareOptions, builtin_options2=cmp_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(3)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) @pytest.mark.parametrize( "direction_enum, relax_op", [ ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_EQ, R.equal, ), ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_NE, R.not_equal, ), ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_GE, R.greater_equal, ), ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_GT, R.greater, ), ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_LE, R.less_equal, ), ( _tfl_stablehlo_comp_dir.StablehloComparisonDirection.STABLEHLO_COMPARISON_DIRECTION_LT, R.less, ), ], ) def test_stablehlo_compare(direction_enum, relax_op): """TFLite StableHLO COMPARE with various comparison directions.""" mod = _load_model_from_buffer(_build_stablehlo_compare_model(direction_enum)) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="bool"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="bool") = relax_op(x, y) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_compare_totalorder_unsupported(): """STABLEHLO_COMPARE with TOTALORDER type raises OpNotImplemented.""" builder = flatbuffers.Builder(1024) _DIR = _tfl_stablehlo_comp_dir.StablehloComparisonDirection _TYPE = _tfl_stablehlo_comp_type.StablehloComparisonType _tfl_stablehlo_compare_opts.StablehloCompareOptionsStart(builder) _tfl_stablehlo_compare_opts.StablehloCompareOptionsAddComparisonDirection( builder, _DIR.STABLEHLO_COMPARISON_DIRECTION_EQ ) _tfl_stablehlo_compare_opts.StablehloCompareOptionsAddCompareType( builder, _TYPE.STABLEHLO_COMPARISON_TYPE_FLOAT_TOTAL_ORDER ) cmp_opts = _tfl_stablehlo_compare_opts.StablehloCompareOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_COMPARE") op_code = _build_operator_code(builder, builtin_op) shape = [2, 2] t_lhs = _build_tensor(builder, 0, shape) t_rhs = _build_tensor(builder, 1, shape) t_out = _build_tensor(builder, 2, shape, tensor_type=_tfl_tensor_type.BOOL) tensors = [t_lhs, t_rhs, t_out] op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloCompareOptions, builtin_options2=cmp_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(3)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="TOTALORDER"): from_tflite(tflite_model) def _stablehlo_gather_i64_vector(builder, start_vector_fn, values): start_vector_fn(builder, len(values)) for value in reversed(values): builder.PrependInt64(value) return builder.EndVector() def _build_stablehlo_gather_model( *, data_shape, indices_shape, output_shape, offset_dims, collapsed_slice_dims, start_index_map, index_vector_dim, slice_sizes, ): """Build a minimal STABLEHLO_GATHER TFLite model.""" builder = flatbuffers.Builder(1024) offset_dims_vec = _stablehlo_gather_i64_vector( builder, _tfl_stablehlo_gather_opts.StablehloGatherOptionsStartOffsetDimsVector, offset_dims, ) collapsed_slice_dims_vec = _stablehlo_gather_i64_vector( builder, _tfl_stablehlo_gather_opts.StablehloGatherOptionsStartCollapsedSliceDimsVector, collapsed_slice_dims, ) start_index_map_vec = _stablehlo_gather_i64_vector( builder, _tfl_stablehlo_gather_opts.StablehloGatherOptionsStartStartIndexMapVector, start_index_map, ) slice_sizes_vec = _stablehlo_gather_i64_vector( builder, _tfl_stablehlo_gather_opts.StablehloGatherOptionsStartSliceSizesVector, slice_sizes, ) _tfl_stablehlo_gather_opts.StablehloGatherOptionsStart(builder) _tfl_stablehlo_gather_opts.StablehloGatherOptionsAddOffsetDims(builder, offset_dims_vec) _tfl_stablehlo_gather_opts.StablehloGatherOptionsAddCollapsedSliceDims( builder, collapsed_slice_dims_vec ) _tfl_stablehlo_gather_opts.StablehloGatherOptionsAddStartIndexMap(builder, start_index_map_vec) _tfl_stablehlo_gather_opts.StablehloGatherOptionsAddIndexVectorDim(builder, index_vector_dim) _tfl_stablehlo_gather_opts.StablehloGatherOptionsAddSliceSizes(builder, slice_sizes_vec) gather_opts = _tfl_stablehlo_gather_opts.StablehloGatherOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_GATHER") op_code = _build_operator_code(builder, builtin_op) t_data = _build_tensor(builder, 0, data_shape) t_indices = _build_tensor(builder, 1, indices_shape, tensor_type=_tfl_tensor_type.INT32) t_out = _build_tensor(builder, 2, output_shape) op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloGatherOptions, builtin_options2=gather_opts, ) subgraph = _build_subgraph( builder, tensors=[t_data, t_indices, t_out], operators=[op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(3)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) @pytest.mark.parametrize( "axis, offset_dims, slice_sizes, output_shape", [ (0, [1], [1, 4], [2, 4]), (1, [0], [3, 1], [3, 2]), ], ) def test_stablehlo_gather_take_equivalent(axis, offset_dims, slice_sizes, output_shape): """TFLite StableHLO GATHER take-equivalent subset.""" mod = _load_model_from_buffer( _build_stablehlo_gather_model( data_shape=[3, 4], indices_shape=[2, 1], output_shape=output_shape, offset_dims=offset_dims, collapsed_slice_dims=[axis], start_index_map=[axis], index_vector_dim=1, slice_sizes=slice_sizes, ) ) out_shape = tuple(output_shape) @I.ir_module class Expected: @R.function def main( data: R.Tensor((3, 4), dtype="float32"), indices: R.Tensor((2, 1), dtype="int32"), ) -> R.Tensor(out_shape, dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): reshaped: R.Tensor((2,), dtype="int32") = R.reshape(indices, (2,)) gv: R.Tensor(out_shape, dtype="float32") = R.take( data, reshaped, axis=axis, mode="fast" ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_gather_complex_unsupported(): """TFLite StableHLO GATHER with multi-dimensional start_index_map is unsupported.""" buf = _build_stablehlo_gather_model( data_shape=[3, 4], indices_shape=[2, 2], output_shape=[2], offset_dims=[], collapsed_slice_dims=[0, 1], start_index_map=[0, 1], index_vector_dim=1, slice_sizes=[1, 1], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="start_index_map"): from_tflite(tflite_model) def _pad_vector(builder, start_vector_fn, values): """Build a FlatBuffers int64 vector for pad options.""" start_vector_fn(builder, len(values)) for v in reversed(values): builder.PrependInt64(v) return builder.EndVector() def _build_stablehlo_pad_model(edge_low, edge_high, interior): """STABLEHLO_PAD with given padding vectors.""" builder = flatbuffers.Builder(1024) lo_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartEdgePaddingLowVector, edge_low, ) hi_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartEdgePaddingHighVector, edge_high, ) int_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartInteriorPaddingVector, interior, ) _tfl_stablehlo_pad_opts.StablehloPadOptionsStart(builder) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddEdgePaddingLow(builder, lo_vec) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddEdgePaddingHigh(builder, hi_vec) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddInteriorPadding(builder, int_vec) pad_opts = _tfl_stablehlo_pad_opts.StablehloPadOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_PAD") op_code = _build_operator_code(builder, builtin_op) t_in = _build_tensor(builder, 0, [3, 3]) # pad_value is a scalar tensor t_pad_val = _build_tensor(builder, 1, []) t_out = _build_tensor(builder, 2, [4, 4]) tensors = [t_in, t_pad_val, t_out] op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloPadOptions, builtin_options2=pad_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0], outputs=[2], ) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array([0.0], dtype=np.float32).tobytes()), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_pad(): """TFLite StableHLO PAD: edge_low=[1,0], edge_high=[0,1], interior=[0,0].""" mod = _load_model_from_buffer( _build_stablehlo_pad_model(edge_low=[1, 0], edge_high=[0, 1], interior=[0, 0]) ) @I.ir_module class Expected: @R.function def main( x: R.Tensor((3, 3), dtype="float32"), ) -> R.Tensor((4, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((4, 4), dtype="float32") = R.nn.pad( x, pad_width=[1, 0, 0, 1], pad_value=0.0 ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_pad_interior_unsupported(): """STABLEHLO_PAD with interior padding raises OpNotImplemented.""" builder = flatbuffers.Builder(1024) lo_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartEdgePaddingLowVector, [0, 0], ) hi_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartEdgePaddingHighVector, [0, 0], ) int_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartInteriorPaddingVector, [1, 0], ) _tfl_stablehlo_pad_opts.StablehloPadOptionsStart(builder) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddEdgePaddingLow(builder, lo_vec) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddEdgePaddingHigh(builder, hi_vec) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddInteriorPadding(builder, int_vec) pad_opts = _tfl_stablehlo_pad_opts.StablehloPadOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_PAD") op_code = _build_operator_code(builder, builtin_op) t_in = _build_tensor(builder, 0, [3, 3]) t_pv = _build_tensor(builder, 1, []) t_out = _build_tensor(builder, 2, [3, 3]) tensors = [t_in, t_pv, t_out] op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloPadOptions, builtin_options2=pad_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0], outputs=[2], ) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array([0.0], dtype=np.float32).tobytes()), _build_buffer(builder), ] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="interior"): from_tflite(tflite_model) def test_stablehlo_pad_negative_unsupported(): """STABLEHLO_PAD with negative edge padding raises OpNotImplemented.""" builder = flatbuffers.Builder(1024) lo_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartEdgePaddingLowVector, [-1, 0], ) hi_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartEdgePaddingHighVector, [0, 0], ) int_vec = _pad_vector( builder, _tfl_stablehlo_pad_opts.StablehloPadOptionsStartInteriorPaddingVector, [0, 0], ) _tfl_stablehlo_pad_opts.StablehloPadOptionsStart(builder) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddEdgePaddingLow(builder, lo_vec) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddEdgePaddingHigh(builder, hi_vec) _tfl_stablehlo_pad_opts.StablehloPadOptionsAddInteriorPadding(builder, int_vec) pad_opts = _tfl_stablehlo_pad_opts.StablehloPadOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_PAD") op_code = _build_operator_code(builder, builtin_op) t_in = _build_tensor(builder, 0, [3, 3]) t_pv = _build_tensor(builder, 1, []) t_out = _build_tensor(builder, 2, [2, 3]) tensors = [t_in, t_pv, t_out] op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloPadOptions, builtin_options2=pad_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0], outputs=[2], ) buffers = [ _build_buffer(builder), _build_buffer(builder, np.array([0.0], dtype=np.float32).tobytes()), _build_buffer(builder), ] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="negative"): from_tflite(tflite_model) def _build_stablehlo_dynamic_slice_model(slice_sizes, start_vals): """STABLEHLO_DYNAMIC_SLICE with given slice sizes and start indices.""" builder = flatbuffers.Builder(1024) ndim = len(slice_sizes) # Build SliceSizes vector _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsStartSliceSizesVector(builder, ndim) for v in reversed(slice_sizes): builder.PrependInt64(v) sizes_vec = builder.EndVector() _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsStart(builder) _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsAddSliceSizes(builder, sizes_vec) dyn_opts = _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_DYNAMIC_SLICE") op_code = _build_operator_code(builder, builtin_op) # operand + start indices + output t_in = _build_tensor(builder, 0, [3, 3]) start_tensors = [] start_inputs = [] start_buffers = [] for i, sv in enumerate(start_vals): bidx = 1 + i start_tensors.append(_build_tensor(builder, bidx, [], tensor_type=_tfl_tensor_type.INT32)) start_inputs.append(bidx) start_buffers.append(_build_buffer(builder, np.array([sv], dtype=np.int32).tobytes())) out_idx = 1 + ndim t_out = _build_tensor(builder, out_idx, slice_sizes) tensors = [t_in, *start_tensors, t_out] op_inputs = [0, *start_inputs] op = _build_operator( builder, 0, op_inputs, [out_idx], builtin_options2_type=_tfl_builtin_options2.StablehloDynamicSliceOptions, builtin_options2=dyn_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=[0], outputs=[out_idx], ) buffers = [_build_buffer(builder), *start_buffers, _build_buffer(builder)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def _build_stablehlo_dynamic_slice_with_dynamic_starts_model(slice_sizes): """STABLEHLO_DYNAMIC_SLICE with runtime start-index inputs.""" builder = flatbuffers.Builder(1024) ndim = len(slice_sizes) _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsStartSliceSizesVector(builder, ndim) for v in reversed(slice_sizes): builder.PrependInt64(v) sizes_vec = builder.EndVector() _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsStart(builder) _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsAddSliceSizes(builder, sizes_vec) dyn_opts = _tfl_stablehlo_dyn_slice_opts.StablehloDynamicSliceOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_DYNAMIC_SLICE") op_code = _build_operator_code(builder, builtin_op) t_in = _build_tensor(builder, 0, [3, 3]) start_tensors = [ _build_tensor(builder, 1 + i, [], tensor_type=_tfl_tensor_type.INT32) for i in range(ndim) ] out_idx = 1 + ndim t_out = _build_tensor(builder, out_idx, slice_sizes) start_inputs = list(range(1, 1 + ndim)) tensors = [t_in, *start_tensors, t_out] op_inputs = [0, *start_inputs] op = _build_operator( builder, 0, op_inputs, [out_idx], builtin_options2_type=_tfl_builtin_options2.StablehloDynamicSliceOptions, builtin_options2=dyn_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=op_inputs, outputs=[out_idx], ) buffers = [_build_buffer(builder) for _ in range(out_idx + 1)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_dynamic_slice(): """TFLite StableHLO DYNAMIC_SLICE: start=[0,1], sizes=[2,2] from (3,3).""" mod = _load_model_from_buffer( _build_stablehlo_dynamic_slice_model(slice_sizes=[2, 2], start_vals=[0, 1]) ) @I.ir_module class Expected: @R.function def main( x: R.Tensor((3, 3), dtype="float32"), ) -> R.Tensor(dtype="float32", ndim=2): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor(dtype="float32", ndim=2) = R.dynamic_strided_slice( x, R.const([0, 1], dtype="int64"), R.const([2, 3], dtype="int64"), R.const([1, 1], dtype="int64"), ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_dynamic_slice_dynamic_starts_unsupported(): """TFLite StableHLO DYNAMIC_SLICE with runtime starts is not supported yet.""" buf = _build_stablehlo_dynamic_slice_with_dynamic_starts_model(slice_sizes=[2, 2]) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="dynamic start"): from_tflite(tflite_model) def test_stablehlo_dynamic_slice_out_of_bounds_unsupported(): """TFLite StableHLO DYNAMIC_SLICE with out-of-bounds starts is not supported.""" buf = _build_stablehlo_dynamic_slice_model(slice_sizes=[2, 2], start_vals=[0, 2]) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="out-of-bounds"): from_tflite(tflite_model) def test_stablehlo_cbrt(): """TFLite StableHLO CBRT uses a sign-preserving composite expression.""" mod = _load_model_from_buffer( _build_stablehlo_model(builtin_name="STABLEHLO_CBRT", input_count=1) ) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.negative(x) lv1: R.Tensor((2, 2), dtype="float32") = R.power(lv, R.const(1.0 / 3.0, "float32")) lv2: R.Tensor((2, 2), dtype="bool") = R.less(x, R.const(0, "float32")) lv3: R.Tensor((2, 2), dtype="float32") = R.negative(lv1) lv4: R.Tensor((2, 2), dtype="float32") = R.power(x, R.const(1.0 / 3.0, "float32")) gv: R.Tensor((2, 2), dtype="float32") = R.where(lv2, lv3, lv4) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_remainder(): """TFLite StableHLO REMAINDER uses truncating remainder semantics.""" mod = _load_model_from_buffer( _build_stablehlo_model(builtin_name="STABLEHLO_REMAINDER", input_count=2) ) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 2), dtype="float32"), y: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.divide(x, y) lv1: R.Tensor((2, 2), dtype="float32") = R.trunc(lv) lv2: R.Tensor((2, 2), dtype="float32") = R.multiply(y, lv1) gv: R.Tensor((2, 2), dtype="float32") = R.subtract(x, lv2) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_stablehlo_dynamic_update_slice_model(start_vals, dynamic_starts=False): """Build a minimal STABLEHLO_DYNAMIC_UPDATE_SLICE model.""" builder = flatbuffers.Builder(1024) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_DYNAMIC_UPDATE_SLICE") op_code = _build_operator_code(builder, builtin_op) t_operand = _build_tensor(builder, 0, [3, 4]) t_update = _build_tensor(builder, 1, [2, 2]) start_tensors = [ _build_tensor(builder, 2 + i, [], tensor_type=_tfl_tensor_type.INT32) for i in range(len(start_vals)) ] out_idx = 2 + len(start_vals) t_out = _build_tensor(builder, out_idx, [3, 4]) tensors = [t_operand, t_update, *start_tensors, t_out] op_inputs = [0, 1, *range(2, out_idx)] op = _build_operator(builder, 0, op_inputs, [out_idx]) subgraph_inputs = op_inputs if dynamic_starts else [0, 1] subgraph = _build_subgraph( builder, tensors=tensors, operators=[op], inputs=subgraph_inputs, outputs=[out_idx], ) if dynamic_starts: buffers = [_build_buffer(builder) for _ in range(out_idx + 1)] else: start_buffers = [ _build_buffer(builder, np.array([start], dtype=np.int32).tobytes()) for start in start_vals ] buffers = [ _build_buffer(builder), _build_buffer(builder), *start_buffers, _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_dynamic_update_slice(): """TFLite StableHLO DYNAMIC_UPDATE_SLICE with static starts.""" mod = _load_model_from_buffer(_build_stablehlo_dynamic_update_slice_model([1, 1])) @I.ir_module class Expected: @R.function def main( operand: R.Tensor((3, 4), dtype="float32"), update: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((3, 4), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((3, 4), dtype="float32") = R.scatter_nd( operand, R.const([[[1, 1], [1, 2]], [[2, 1], [2, 2]]], dtype="int64"), update, reduction="update", ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_dynamic_update_slice_dynamic_starts(): """TFLite StableHLO DYNAMIC_UPDATE_SLICE with runtime starts lowers structurally.""" mod = _load_model_from_buffer( _build_stablehlo_dynamic_update_slice_model([0, 0], dynamic_starts=True) ) @I.ir_module class Expected: @R.function def main( operand: R.Tensor((3, 4), dtype="float32"), update: R.Tensor((2, 2), dtype="float32"), s0: R.Tensor((), dtype="int32"), s1: R.Tensor((), dtype="int32"), ) -> R.Tensor((3, 4), dtype="float32"): R.func_attr({"num_input": 4}) with R.dataflow(): lv: R.Tensor((2,), dtype="int64") = R.arange(0, 2, 1, dtype="int64") lv1: R.Tensor((), dtype="int64") = R.astype(s0, dtype="int64") lv2: R.Tensor((), dtype="int64") = R.maximum(lv1, R.const(0, "int64")) lv3: R.Tensor((), dtype="int64") = R.minimum(lv2, R.const(1, "int64")) lv4: R.Tensor((2,), dtype="int64") = R.add(lv, lv3) lv5: R.Tensor((2, 1), dtype="int64") = R.reshape(lv4, (2, 1)) lv6: R.Tensor((2, 2), dtype="int64") = R.broadcast_to(lv5, (2, 2)) lv7: R.Tensor((2,), dtype="int64") = R.arange(0, 2, 1, dtype="int64") lv8: R.Tensor((), dtype="int64") = R.astype(s1, dtype="int64") lv9: R.Tensor((), dtype="int64") = R.maximum(lv8, R.const(0, "int64")) lv10: R.Tensor((), dtype="int64") = R.minimum(lv9, R.const(2, "int64")) lv11: R.Tensor((2,), dtype="int64") = R.add(lv7, lv10) lv12: R.Tensor((1, 2), dtype="int64") = R.reshape(lv11, (1, 2)) lv13: R.Tensor((2, 2), dtype="int64") = R.broadcast_to(lv12, (2, 2)) lv14: R.Tensor((2, 2, 1), dtype="int64") = R.expand_dims(lv6, axis=[-1]) lv15: R.Tensor((2, 2, 1), dtype="int64") = R.expand_dims(lv13, axis=[-1]) lv16: R.Tensor((2, 2, 2), dtype="int64") = R.concat((lv14, lv15), axis=-1) gv: R.Tensor((3, 4), dtype="float32") = R.scatter_nd( operand, lv16, update, reduction="update" ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_dynamic_update_slice_out_of_bounds_unsupported(): """TFLite StableHLO DYNAMIC_UPDATE_SLICE rejects out-of-bounds updates.""" buf = _build_stablehlo_dynamic_update_slice_model([2, 3]) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="out-of-bounds"): from_tflite(tflite_model) def _build_stablehlo_dot_general_model(lhs_contract, rhs_contract, lhs_batch=None, rhs_batch=None): """Build a minimal STABLEHLO_DOT_GENERAL model.""" builder = flatbuffers.Builder(1024) lhs_batch = [] if lhs_batch is None else lhs_batch rhs_batch = [] if rhs_batch is None else rhs_batch lhs_batch_vec = _tflite_int64_vector( builder, _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsStartLhsBatchingDimensionsVector, lhs_batch, ) rhs_batch_vec = _tflite_int64_vector( builder, _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsStartRhsBatchingDimensionsVector, rhs_batch, ) lhs_contract_vec = _tflite_int64_vector( builder, _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsStartLhsContractingDimensionsVector, lhs_contract, ) rhs_contract_vec = _tflite_int64_vector( builder, _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsStartRhsContractingDimensionsVector, rhs_contract, ) _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsStart(builder) _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsAddLhsBatchingDimensions( builder, lhs_batch_vec ) _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsAddRhsBatchingDimensions( builder, rhs_batch_vec ) _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsAddLhsContractingDimensions( builder, lhs_contract_vec ) _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsAddRhsContractingDimensions( builder, rhs_contract_vec ) dot_opts = _tfl_stablehlo_dot_opts.StablehloDotGeneralOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_DOT_GENERAL") op_code = _build_operator_code(builder, builtin_op) t_lhs = _build_tensor(builder, 0, [2, 3]) t_rhs = _build_tensor(builder, 1, [3, 4]) t_out = _build_tensor(builder, 2, [2, 4]) op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloDotGeneralOptions, builtin_options2=dot_opts, ) subgraph = _build_subgraph( builder, tensors=[t_lhs, t_rhs, t_out], operators=[op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(3)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_dot_general(): """TFLite StableHLO DOT_GENERAL canonical 2D matmul.""" mod = _load_model_from_buffer(_build_stablehlo_dot_general_model([1], [0])) @I.ir_module class Expected: @R.function def main( lhs: R.Tensor((2, 3), dtype="float32"), rhs: R.Tensor((3, 4), dtype="float32"), ) -> R.Tensor((2, 4), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((2, 4), dtype="float32") = R.matmul(lhs, rhs) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_dot_general_noncanonical_unsupported(): """TFLite StableHLO DOT_GENERAL rejects non-canonical contracting dims.""" buf = _build_stablehlo_dot_general_model([0], [0]) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="contracting"): from_tflite(tflite_model) def _build_stablehlo_convolution_model(feature_group_count=1, input_batch_dimension=0): """Build a minimal STABLEHLO_CONVOLUTION model.""" builder = flatbuffers.Builder(1024) window_strides_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartWindowStridesVector, [1, 1], ) padding_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartPaddingVector, [0, 0, 0, 0], ) lhs_dilation_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartLhsDilationVector, [1, 1] ) rhs_dilation_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartRhsDilationVector, [1, 1] ) window_reversal_vec = _tflite_bool_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartWindowReversalVector, [False, False], ) input_spatial_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartInputSpatialDimensionsVector, [1, 2], ) kernel_spatial_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartKernelSpatialDimensionsVector, [0, 1], ) output_spatial_vec = _tflite_int64_vector( builder, _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStartOutputSpatialDimensionsVector, [1, 2], ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsStart(builder) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddWindowStrides( builder, window_strides_vec ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddPadding(builder, padding_vec) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddLhsDilation(builder, lhs_dilation_vec) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddRhsDilation(builder, rhs_dilation_vec) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddWindowReversal( builder, window_reversal_vec ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddInputBatchDimension( builder, input_batch_dimension ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddInputFeatureDimension(builder, 3) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddInputSpatialDimensions( builder, input_spatial_vec ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddKernelInputFeatureDimension(builder, 2) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddKernelOutputFeatureDimension(builder, 3) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddKernelSpatialDimensions( builder, kernel_spatial_vec ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddOutputBatchDimension(builder, 0) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddOutputFeatureDimension(builder, 3) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddOutputSpatialDimensions( builder, output_spatial_vec ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddFeatureGroupCount( builder, feature_group_count ) _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsAddBatchGroupCount(builder, 1) conv_opts = _tfl_stablehlo_conv_opts.StablehloConvolutionOptionsEnd(builder) builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_CONVOLUTION") op_code = _build_operator_code(builder, builtin_op) t_data = _build_tensor(builder, 0, [1, 5, 5, 2]) t_kernel = _build_tensor(builder, 1, [3, 3, 2, 4]) t_out = _build_tensor(builder, 2, [1, 3, 3, 4]) op = _build_operator( builder, 0, [0, 1], [2], builtin_options2_type=_tfl_builtin_options2.StablehloConvolutionOptions, builtin_options2=conv_opts, ) subgraph = _build_subgraph( builder, tensors=[t_data, t_kernel, t_out], operators=[op], inputs=[0, 1], outputs=[2], ) buffers = [_build_buffer(builder) for _ in range(3)] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[op_code], buffers=buffers ) def test_stablehlo_convolution(): """TFLite StableHLO CONVOLUTION canonical NHWC/HWIO 2D convolution.""" mod = _load_model_from_buffer(_build_stablehlo_convolution_model()) @I.ir_module class Expected: @R.function def main( data: R.Tensor((1, 5, 5, 2), dtype="float32"), kernel: R.Tensor((3, 3, 2, 4), dtype="float32"), ) -> R.Tensor((1, 3, 3, 4), dtype="float32"): R.func_attr({"num_input": 2}) with R.dataflow(): gv: R.Tensor((1, 3, 3, 4), dtype="float32") = R.nn.conv2d( data, kernel, strides=[1, 1], padding=[0, 0, 0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_stablehlo_convolution_feature_group_unsupported(): """TFLite StableHLO CONVOLUTION rejects grouped convolution in the first subset.""" buf = _build_stablehlo_convolution_model(feature_group_count=2) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="feature_group_count"): from_tflite(tflite_model) def test_stablehlo_convolution_dimension_numbers_unsupported(): """TFLite StableHLO CONVOLUTION rejects non-canonical dimension numbers.""" buf = _build_stablehlo_convolution_model(input_batch_dimension=1) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="dimension numbers"): from_tflite(tflite_model) # Quantized TFLite QDQ tests def test_tensor_quantization_parameters_are_parsed(): """Tensor quantization metadata is kept without requiring quantized op support.""" builder = flatbuffers.Builder(1024) per_tensor_quantization = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) per_axis_quantization = _build_quantization_parameters( builder, scale=[0.25, 0.75], zero_point=[0, 0], quantized_dimension=3 ) per_tensor = _build_tensor( builder, 0, [1, 4], tensor_type=_tfl_tensor_type.UINT8, quantization=per_tensor_quantization, ) per_axis = _build_tensor( builder, 1, [1, 2, 3, 2], tensor_type=_tfl_tensor_type.INT8, quantization=per_axis_quantization, ) subgraph = _build_subgraph( builder, tensors=[per_tensor, per_axis], operators=[], inputs=[0, 1], outputs=[0, 1] ) buffers = [_build_buffer(builder), _build_buffer(builder)] buf = _finish_tflite_model(builder, subgraph=subgraph, operator_codes=[], buffers=buffers) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) converter = tflite_frontend.OperatorConverter( tflite_model, tflite_model.Subgraphs(0), tflite_frontend.ExprTable(), None ) per_tensor_wrapper, per_axis_wrapper = converter.get_tensors([0, 1]) np.testing.assert_allclose(per_tensor_wrapper.qnn_params["scale"].data.numpy(), 0.5) np.testing.assert_equal(per_tensor_wrapper.qnn_params["zero_point"].data.numpy(), 3) assert per_tensor_wrapper.qnn_params["axis"] == 0 np.testing.assert_allclose( per_axis_wrapper.qnn_params["scale"].data.numpy(), np.array([0.25, 0.75]) ) np.testing.assert_equal(per_axis_wrapper.qnn_params["zero_point"].data.numpy(), 0) assert per_axis_wrapper.qnn_params["axis"] == 3 mod = from_tflite(tflite_model) assert len(mod["main"].params) == 2 def test_quantize_op_uses_relax_quantize(): """TFLite QUANTIZE float32 -> int8 uses R.quantize.""" builder = flatbuffers.Builder(1024) input_data = np.array([1.0, 2.0], dtype=np.float32) output_qparams = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) input_tensor = _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.FLOAT32) output_tensor = _build_tensor( builder, 1, [2], tensor_type=_tfl_tensor_type.INT8, quantization=output_qparams, ) quantize_op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph( builder, tensors=[input_tensor, output_tensor], operators=[quantize_op], inputs=[0], outputs=[1], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.QUANTIZE)] input_buffer = _build_buffer(builder, input_data.tobytes()) output_buffer = _build_buffer(builder) buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[input_buffer, output_buffer], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main(x: R.Tensor((2,), dtype="float32")) -> R.Tensor((2,), dtype="int8"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2,), dtype="int8") = R.quantize( x, R.const(0.5, "float32"), R.const(3, "int32"), axis=0, out_dtype="int8", ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantize_op_requantize_uses_dq_q(): """TFLite QUANTIZE with quantized input uses DQ→Q (requantize).""" builder = flatbuffers.Builder(1024) input_data = np.array([10, 20], dtype=np.int8) input_qparams = _build_quantization_parameters( builder, scale=[0.25], zero_point=[1], quantized_dimension=0 ) output_qparams = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) input_tensor = _build_tensor( builder, 0, [2], tensor_type=_tfl_tensor_type.INT8, quantization=input_qparams, ) output_tensor = _build_tensor( builder, 1, [2], tensor_type=_tfl_tensor_type.INT8, quantization=output_qparams, ) quantize_op = _build_operator( builder, 0, [0], [1], ) subgraph = _build_subgraph( builder, tensors=[input_tensor, output_tensor], operators=[quantize_op], inputs=[0], outputs=[1], ) operator_codes = [ _build_operator_code(builder, _tfl_builtin_operator.QUANTIZE), ] input_buffer = _build_buffer(builder, input_data.tobytes()) output_buffer = _build_buffer(builder) buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[input_buffer, output_buffer], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2,), dtype="int8"), ) -> R.Tensor((2,), dtype="int8"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.25, "float32"), R.const(1, "int32"), out_dtype="float32", axis=0, ) gv: R.Tensor((2,), dtype="int8") = R.quantize( lv, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_dequantize_op_uses_relax_dequantize(): """TFLite DEQUANTIZE int8 -> float32 uses R.dequantize.""" builder = flatbuffers.Builder(1024) input_data = np.array([10, 20], dtype=np.int8) input_qparams = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) input_tensor = _build_tensor( builder, 0, [2], tensor_type=_tfl_tensor_type.INT8, quantization=input_qparams, ) output_tensor = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.FLOAT32) dequantize_op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph( builder, tensors=[input_tensor, output_tensor], operators=[dequantize_op], inputs=[0], outputs=[1], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.DEQUANTIZE)] input_buffer = _build_buffer(builder, input_data.tobytes()) output_buffer = _build_buffer(builder) buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[input_buffer, output_buffer], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main(x: R.Tensor((2,), dtype="int8")) -> R.Tensor((2,), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2,), dtype="float32") = R.dequantize( x, R.const(0.5, "float32"), R.const(3, "int32"), axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_dequantize_float16_uses_astype(): """TFLite DEQUANTIZE float16 -> float32 uses R.astype.""" builder = flatbuffers.Builder(1024) input_data = np.array([1.5, -2.0], dtype=np.float16) input_tensor = _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.FLOAT16) output_tensor = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.FLOAT32) dequantize_op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph( builder, tensors=[input_tensor, output_tensor], operators=[dequantize_op], inputs=[], outputs=[1], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.DEQUANTIZE)] input_buffer = _build_buffer(builder, input_data.tobytes()) output_buffer = _build_buffer(builder) buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[input_buffer, output_buffer], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main() -> R.Tensor((2,), dtype="float32"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((2,), dtype="float32") = R.astype( R.const(np.array([1.5, -2.0], dtype=np.float16)), dtype="float32" ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_avg_pool2d_uses_astype(): """Quantized AVERAGE_POOL_2D casts through int32 with R.astype.""" builder = flatbuffers.Builder(1024) qparams = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) input_tensor = _build_tensor( builder, 0, [1, 2, 2, 1], tensor_type=_tfl_tensor_type.INT8, quantization=qparams, ) output_tensor = _build_tensor( builder, 1, [1, 1, 1, 1], tensor_type=_tfl_tensor_type.INT8, quantization=qparams, ) _tfl_pool2d_options.Pool2DOptionsStart(builder) _tfl_pool2d_options.Pool2DOptionsAddPadding(builder, _tfl_padding.VALID) _tfl_pool2d_options.Pool2DOptionsAddStrideH(builder, 1) _tfl_pool2d_options.Pool2DOptionsAddStrideW(builder, 1) _tfl_pool2d_options.Pool2DOptionsAddFilterHeight(builder, 2) _tfl_pool2d_options.Pool2DOptionsAddFilterWidth(builder, 2) _tfl_pool2d_options.Pool2DOptionsAddFusedActivationFunction(builder, _tfl_activation_fn.NONE) pool_opts = _tfl_pool2d_options.Pool2DOptionsEnd(builder) avg_pool_op = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.Pool2DOptions, builtin_options=pool_opts, ) subgraph = _build_subgraph( builder, tensors=[input_tensor, output_tensor], operators=[avg_pool_op], inputs=[0], outputs=[1], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.AVERAGE_POOL_2D)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder), _build_buffer(builder)], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) subgraph = tflite_model.Subgraphs(0) bb = relax.BlockBuilder() exp_tab = tflite_frontend.ExprTable() input_var = relax.Var("tvmgen_tensor_0", relax.TensorType((1, 2, 2, 1), dtype="int8")) exp_tab.set_expr("tvmgen_tensor_0", input_var) converter = tflite_frontend.OperatorConverter(tflite_model, subgraph, exp_tab, bb) with bb.function("main", [input_var]): with bb.dataflow(): output = converter.convert_pool2d(subgraph.Operators(0), "average") gv = bb.emit_output(output) bb.emit_func_output(gv) mod = bb.get() @I.ir_module class Expected: @R.function def main(tvmgen_tensor_0: R.Tensor((1, 2, 2, 1), dtype="int8")) -> R.Tensor( (1, 1, 1, 1), dtype="int8" ): with R.dataflow(): lv: R.Tensor((1, 2, 2, 1), dtype="int32") = R.astype(tvmgen_tensor_0, dtype="int32") lv1: R.Tensor((1, 1, 1, 1), dtype="int32") = R.nn.avg_pool2d( lv, pool_size=[2, 2], strides=[1, 1], dilation=[1, 1], padding=[0, 0, 0, 0], ceil_mode=False, count_include_pad=False, layout="NHWC", out_layout="NHWC", ) gv: R.Tensor((1, 1, 1, 1), dtype="int8") = R.astype(lv1, dtype="int8") R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_conv2d_per_tensor_uses_qdq(): """Quantized Conv2D with per-tensor quantization uses DQ -> conv2d -> Q.""" builder = flatbuffers.Builder(2048) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) wt_q = _build_quantization_parameters( builder, scale=[0.25], zero_point=[0], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) input_tensor = _build_tensor( builder, 0, [1, 4, 4, 1], tensor_type=_tfl_tensor_type.INT8, quantization=in_q, ) weight_tensor = _build_tensor( builder, 1, [2, 3, 3, 1], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q, ) output_tensor = _build_tensor( builder, 2, [1, 2, 2, 2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q, ) _tfl_conv2d_options.Conv2DOptionsStart(builder) _tfl_conv2d_options.Conv2DOptionsAddStrideH(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddStrideW(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddPadding(builder, _tfl_padding.VALID) _tfl_conv2d_options.Conv2DOptionsAddFusedActivationFunction(builder, 0) conv_opts = _tfl_conv2d_options.Conv2DOptionsEnd(builder) conv_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.Conv2DOptions, builtin_options=conv_opts, ) subgraph = _build_subgraph( builder, tensors=[input_tensor, weight_tensor, output_tensor], operators=[conv_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.CONV_2D)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder), _build_buffer(builder), _build_buffer(builder)], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 4, 4, 1), dtype="int8"), tvmgen_tensor_1: R.Tensor((2, 3, 3, 1), dtype="int8"), ) -> R.Tensor((1, 2, 2, 2), dtype="int8"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((1, 4, 4, 1), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((3, 3, 1, 2), dtype="int8") = R.permute_dims( tvmgen_tensor_1, axes=[1, 2, 3, 0], ) lv2: R.Tensor((3, 3, 1, 2), dtype="float32") = R.dequantize( lv1, R.const(0.25, "float32"), R.const(0, "int32"), out_dtype="float32", axis=3, ) lv3: R.Tensor((1, 2, 2, 2), dtype="float32") = R.nn.conv2d( lv, lv2, strides=[1, 1], padding=[0, 0, 0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) gv: R.Tensor((1, 2, 2, 2), dtype="int8") = R.quantize( lv3, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_conv2d_per_channel_weight_uses_remapped_axis(): """Quantized Conv2D remaps per-channel weight axis after OHWI -> HWIO.""" builder = flatbuffers.Builder(2048) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) wt_q = _build_quantization_parameters( builder, scale=[0.25, 0.75], zero_point=[0, 0], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) input_tensor = _build_tensor( builder, 0, [1, 4, 4, 1], tensor_type=_tfl_tensor_type.INT8, quantization=in_q, ) weight_tensor = _build_tensor( builder, 1, [2, 3, 3, 1], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q, ) output_tensor = _build_tensor( builder, 2, [1, 2, 2, 2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q, ) _tfl_conv2d_options.Conv2DOptionsStart(builder) _tfl_conv2d_options.Conv2DOptionsAddStrideH(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddStrideW(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddPadding(builder, _tfl_padding.VALID) _tfl_conv2d_options.Conv2DOptionsAddFusedActivationFunction(builder, 0) conv_opts = _tfl_conv2d_options.Conv2DOptionsEnd(builder) conv_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.Conv2DOptions, builtin_options=conv_opts, ) subgraph = _build_subgraph( builder, tensors=[input_tensor, weight_tensor, output_tensor], operators=[conv_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.CONV_2D)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder), _build_buffer(builder), _build_buffer(builder)], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 4, 4, 1), dtype="int8"), tvmgen_tensor_1: R.Tensor((2, 3, 3, 1), dtype="int8"), ) -> R.Tensor((1, 2, 2, 2), dtype="int8"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((1, 4, 4, 1), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((3, 3, 1, 2), dtype="int8") = R.permute_dims( tvmgen_tensor_1, axes=[1, 2, 3, 0], ) lv2: R.Tensor((3, 3, 1, 2), dtype="float32") = R.dequantize( lv1, R.const([0.25, 0.75], "float32"), R.const(0, "int32"), out_dtype="float32", axis=3, ) lv3: R.Tensor((1, 2, 2, 2), dtype="float32") = R.nn.conv2d( lv, lv2, strides=[1, 1], padding=[0, 0, 0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) gv: R.Tensor((1, 2, 2, 2), dtype="int8") = R.quantize( lv3, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_concat_uses_qdq(): """Quantized CONCATENATION uses DQ each input → concat → Q.""" import flatbuffers import tflite.Model builder = flatbuffers.Builder(1024) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) t0 = _build_tensor(builder, 0, [1, 2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t1 = _build_tensor(builder, 1, [1, 2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t2 = _build_tensor(builder, 2, [1, 4], tensor_type=_tfl_tensor_type.INT8, quantization=out_q) _tfl_concatenation_options.ConcatenationOptionsStart(builder) _tfl_concatenation_options.ConcatenationOptionsAddAxis(builder, 1) _tfl_concatenation_options.ConcatenationOptionsAddFusedActivationFunction(builder, 0) concat_opts = _tfl_concatenation_options.ConcatenationOptionsEnd(builder) concat_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.ConcatenationOptions, builtin_options=concat_opts, ) subgraph = _build_subgraph( builder, tensors=[t0, t1, t2], operators=[concat_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.CONCATENATION)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 3, ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 2), dtype="int8"), tvmgen_tensor_1: R.Tensor((1, 2), dtype="int8"), ) -> R.Tensor((1, 4), dtype="int8"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((1, 2), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((1, 2), dtype="float32") = R.dequantize( tvmgen_tensor_1, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv2: R.Tensor((1, 4), dtype="float32") = R.concat((lv, lv1), axis=1) gv: R.Tensor((1, 4), dtype="int8") = R.quantize( lv2, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_concat_fused_relu_uses_quantized_clip(): """Quantized CONCATENATION fused RELU clips in the quantized domain.""" builder = flatbuffers.Builder(1024) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) t0 = _build_tensor(builder, 0, [1, 2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t1 = _build_tensor(builder, 1, [1, 2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t2 = _build_tensor(builder, 2, [1, 4], tensor_type=_tfl_tensor_type.INT8, quantization=out_q) _tfl_concatenation_options.ConcatenationOptionsStart(builder) _tfl_concatenation_options.ConcatenationOptionsAddAxis(builder, 1) _tfl_concatenation_options.ConcatenationOptionsAddFusedActivationFunction( builder, _tfl_activation_fn.RELU ) concat_opts = _tfl_concatenation_options.ConcatenationOptionsEnd(builder) concat_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.ConcatenationOptions, builtin_options=concat_opts, ) subgraph = _build_subgraph( builder, tensors=[t0, t1, t2], operators=[concat_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.CONCATENATION)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 3, ) mod = _load_model_from_buffer(buf) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 2), dtype="int8"), tvmgen_tensor_1: R.Tensor((1, 2), dtype="int8"), ) -> R.Tensor((1, 4), dtype="int8"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((1, 2), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((1, 2), dtype="float32") = R.dequantize( tvmgen_tensor_1, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv2: R.Tensor((1, 4), dtype="float32") = R.concat((lv, lv1), axis=1) lv3: R.Tensor((1, 4), dtype="int8") = R.quantize( lv2, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="int8", axis=0, ) gv: R.Tensor((1, 4), dtype="int8") = R.clip(lv3, min=3.0, max=127.0) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_add_uses_qdq(): """Quantized ADD uses DQ each input -> add -> Q.""" builder = flatbuffers.Builder(1024) lhs_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) rhs_q = _build_quantization_parameters( builder, scale=[0.25], zero_point=[1], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_lhs = _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.INT8, quantization=lhs_q) t_rhs = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT8, quantization=rhs_q) t_out = _build_tensor(builder, 2, [2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q) _tfl_add_options.AddOptionsStart(builder) _tfl_add_options.AddOptionsAddFusedActivationFunction(builder, 0) add_opts = _tfl_add_options.AddOptionsEnd(builder) add_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.AddOptions, builtin_options=add_opts, ) subgraph = _build_subgraph( builder, tensors=[t_lhs, t_rhs, t_out], operators=[add_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.ADD)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 3, ) mod = _load_model_from_buffer(buf) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2,), dtype="int8"), tvmgen_tensor_1: R.Tensor((2,), dtype="int8"), ) -> R.Tensor((2,), dtype="int8"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_1, R.const(0.25, "float32"), R.const(1, "int32"), out_dtype="float32", axis=0, ) lv2: R.Tensor((2,), dtype="float32") = R.add(lv, lv1) gv: R.Tensor((2,), dtype="int8") = R.quantize( lv2, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_add_fused_relu6_uses_float_clip_before_quantize(): """Quantized ADD fused RELU6 applies the activation before quantizing.""" builder = flatbuffers.Builder(1024) lhs_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) rhs_q = _build_quantization_parameters( builder, scale=[0.25], zero_point=[1], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_lhs = _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.INT8, quantization=lhs_q) t_rhs = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT8, quantization=rhs_q) t_out = _build_tensor(builder, 2, [2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q) _tfl_add_options.AddOptionsStart(builder) _tfl_add_options.AddOptionsAddFusedActivationFunction(builder, _tfl_activation_fn.RELU6) add_opts = _tfl_add_options.AddOptionsEnd(builder) add_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.AddOptions, builtin_options=add_opts, ) subgraph = _build_subgraph( builder, tensors=[t_lhs, t_rhs, t_out], operators=[add_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.ADD)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 3, ) mod = _load_model_from_buffer(buf) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2,), dtype="int8"), tvmgen_tensor_1: R.Tensor((2,), dtype="int8"), ) -> R.Tensor((2,), dtype="int8"): R.func_attr({"num_input": 2}) with R.dataflow(): lv: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_1, R.const(0.25, "float32"), R.const(1, "int32"), out_dtype="float32", axis=0, ) lv2: R.Tensor((2,), dtype="float32") = R.add(lv, lv1) lv3: R.Tensor((2,), dtype="float32") = R.clip(lv2, min=0, max=6) gv: R.Tensor((2,), dtype="int8") = R.quantize( lv3, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_add_without_output_qparams_invalid(): """Quantized ADD with missing output qparams raises OpAttributeInvalid.""" builder = flatbuffers.Builder(1024) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) t_lhs = _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t_rhs = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t_out = _build_tensor(builder, 2, [2], tensor_type=_tfl_tensor_type.INT8) _tfl_add_options.AddOptionsStart(builder) _tfl_add_options.AddOptionsAddFusedActivationFunction(builder, _tfl_activation_fn.NONE) add_opts = _tfl_add_options.AddOptionsEnd(builder) add_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.AddOptions, builtin_options=add_opts, ) subgraph = _build_subgraph( builder, tensors=[t_lhs, t_rhs, t_out], operators=[add_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.ADD)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 3, ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpAttributeInvalid, match="output must have quantization"): from_tflite(tflite_model) def test_quantized_square_unsupported(): """Quantized SQUARE is rejected instead of applying integer power directly.""" builder = flatbuffers.Builder(1024) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_in = _build_tensor(builder, 0, [2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t_out = _build_tensor(builder, 1, [2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q) square_op = _build_operator(builder, 0, [0], [1]) subgraph = _build_subgraph( builder, tensors=[t_in, t_out], operators=[square_op], inputs=[0], outputs=[1], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.SQUARE)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 2, ) with pytest.raises(tvm.error.OpNotImplemented, match="SQUARE"): _load_model_from_buffer(buf) def test_quantized_conv2d_with_int32_bias_dequantizes_bias(): """Conv2D with INT32 bias dequantizes bias with in_scale x wt_scale.""" import flatbuffers import tflite.Model builder = flatbuffers.Builder(2048) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) wt_q = _build_quantization_parameters( builder, scale=[0.25], zero_point=[0], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_in = _build_tensor( builder, 0, [1, 4, 4, 1], tensor_type=_tfl_tensor_type.INT8, quantization=in_q ) t_wt = _build_tensor( builder, 1, [2, 3, 3, 1], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q ) t_bi = _build_tensor(builder, 2, [2], tensor_type=_tfl_tensor_type.INT32) t_ou = _build_tensor( builder, 3, [1, 2, 2, 2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q ) _tfl_conv2d_options.Conv2DOptionsStart(builder) _tfl_conv2d_options.Conv2DOptionsAddStrideH(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddStrideW(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddPadding(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddFusedActivationFunction(builder, 0) conv_opts = _tfl_conv2d_options.Conv2DOptionsEnd(builder) conv_op = _build_operator( builder, 0, [0, 1, 2], [3], builtin_options_type=_tfl_builtin_options.Conv2DOptions, builtin_options=conv_opts, ) subgraph = _build_subgraph( builder, tensors=[t_in, t_wt, t_bi, t_ou], operators=[conv_op], inputs=[0, 1, 2], outputs=[3], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.CONV_2D)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 4, ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 4, 4, 1), dtype="int8"), tvmgen_tensor_1: R.Tensor((2, 3, 3, 1), dtype="int8"), tvmgen_tensor_2: R.Tensor((2,), dtype="int32"), ) -> R.Tensor((1, 2, 2, 2), dtype="int8"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((1, 4, 4, 1), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((3, 3, 1, 2), dtype="int8") = R.permute_dims( tvmgen_tensor_1, axes=[1, 2, 3, 0], ) lv2: R.Tensor((3, 3, 1, 2), dtype="float32") = R.dequantize( lv1, R.const(0.25, "float32"), R.const(0, "int32"), out_dtype="float32", axis=3, ) lv3: R.Tensor((1, 2, 2, 2), dtype="float32") = R.nn.conv2d( lv, lv2, strides=[1, 1], padding=[0, 0, 0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) lv4: R.Tensor((), dtype="float32") = R.multiply( R.const(0.5, "float32"), R.const(0.25, "float32"), ) lv5: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_2, lv4, R.const(0, "int32"), out_dtype="float32", axis=0, ) lv6: R.Tensor((1, 2, 2, 2), dtype="float32") = R.add(lv3, lv5) gv: R.Tensor((1, 2, 2, 2), dtype="int8") = R.quantize( lv6, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_conv2d_per_channel_weight_with_int32_bias_dequantizes_bias(): """Conv2D with per-channel weight quantization uses vector bias scale.""" builder = flatbuffers.Builder(2048) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) wt_q = _build_quantization_parameters( builder, scale=[0.25, 0.75], zero_point=[0, 0], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_in = _build_tensor( builder, 0, [1, 4, 4, 1], tensor_type=_tfl_tensor_type.INT8, quantization=in_q ) t_wt = _build_tensor( builder, 1, [2, 3, 3, 1], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q ) t_bi = _build_tensor(builder, 2, [2], tensor_type=_tfl_tensor_type.INT32) t_ou = _build_tensor( builder, 3, [1, 2, 2, 2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q ) _tfl_conv2d_options.Conv2DOptionsStart(builder) _tfl_conv2d_options.Conv2DOptionsAddStrideH(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddStrideW(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddPadding(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddFusedActivationFunction(builder, 0) conv_opts = _tfl_conv2d_options.Conv2DOptionsEnd(builder) conv_op = _build_operator( builder, 0, [0, 1, 2], [3], builtin_options_type=_tfl_builtin_options.Conv2DOptions, builtin_options=conv_opts, ) subgraph = _build_subgraph( builder, tensors=[t_in, t_wt, t_bi, t_ou], operators=[conv_op], inputs=[0, 1, 2], outputs=[3], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.CONV_2D)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 4, ) mod = _load_model_from_buffer(buf) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 4, 4, 1), dtype="int8"), tvmgen_tensor_1: R.Tensor((2, 3, 3, 1), dtype="int8"), tvmgen_tensor_2: R.Tensor((2,), dtype="int32"), ) -> R.Tensor((1, 2, 2, 2), dtype="int8"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((1, 4, 4, 1), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((3, 3, 1, 2), dtype="int8") = R.permute_dims( tvmgen_tensor_1, axes=[1, 2, 3, 0], ) lv2: R.Tensor((3, 3, 1, 2), dtype="float32") = R.dequantize( lv1, R.const([0.25, 0.75], "float32"), R.const(0, "int32"), out_dtype="float32", axis=3, ) lv3: R.Tensor((1, 2, 2, 2), dtype="float32") = R.nn.conv2d( lv, lv2, strides=[1, 1], padding=[0, 0, 0, 0], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) lv4: R.Tensor((2,), dtype="float32") = R.multiply( R.const(0.5, "float32"), R.const([0.25, 0.75], "float32"), ) lv5: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_2, lv4, R.const(0, "int32"), out_dtype="float32", axis=0, ) lv6: R.Tensor((1, 2, 2, 2), dtype="float32") = R.add(lv3, lv5) gv: R.Tensor((1, 2, 2, 2), dtype="int8") = R.quantize( lv6, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_per_channel_depthwise_conv_unsupported(): """Per-channel quantized depthwise Conv2D raises OpNotImplemented.""" import flatbuffers import tflite.Model builder = flatbuffers.Builder(1024) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[0], quantized_dimension=0 ) # Per-channel weight: 2 channels, scale vector length 2 wt_q = _build_quantization_parameters( builder, scale=[0.25, 0.75], zero_point=[0, 0], quantized_dimension=3 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_in = _build_tensor( builder, 0, [1, 4, 4, 2], tensor_type=_tfl_tensor_type.INT8, quantization=in_q ) t_wt = _build_tensor( builder, 1, [1, 3, 3, 2], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q ) t_ou = _build_tensor( builder, 2, [1, 2, 2, 2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q ) _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsStart(builder) _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsAddStrideH(builder, 1) _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsAddStrideW(builder, 1) _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsAddDepthMultiplier(builder, 1) _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsAddPadding(builder, 1) _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsAddFusedActivationFunction(builder, 0) dw_opts = _tfl_depthwise_conv2d_options.DepthwiseConv2DOptionsEnd(builder) dw_op = _build_operator( builder, 0, [0, 1], [2], builtin_options_type=_tfl_builtin_options.DepthwiseConv2DOptions, builtin_options=dw_opts, ) subgraph = _build_subgraph( builder, tensors=[t_in, t_wt, t_ou], operators=[dw_op], inputs=[0, 1], outputs=[2], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.DEPTHWISE_CONV_2D)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 3, ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) with pytest.raises(tvm.error.OpNotImplemented, match="Per-channel"): from_tflite(tflite_model) def test_uint8_reshape_requantize_uses_dq_reshape_q(): """uint8 RESHAPE with different qparams uses DQ→reshape→Q.""" import flatbuffers import numpy as np import tflite.Model builder = flatbuffers.Builder(1024) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[128], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[100], quantized_dimension=0 ) t_in = _build_tensor(builder, 0, [1, 4], tensor_type=_tfl_tensor_type.UINT8, quantization=in_q) t_ou = _build_tensor(builder, 1, [2, 2], tensor_type=_tfl_tensor_type.UINT8, quantization=out_q) # Use ReshapeOptions with static new_shape [2, 2] new_shape_np = np.array([2, 2], dtype=np.int32) new_shape_vec = _tflite_int32_vector( builder, _tfl_reshape_options.ReshapeOptionsStartNewShapeVector, new_shape_np ) _tfl_reshape_options.ReshapeOptionsStart(builder) _tfl_reshape_options.ReshapeOptionsAddNewShape(builder, new_shape_vec) reshape_opts = _tfl_reshape_options.ReshapeOptionsEnd(builder) reshape_op = _build_operator( builder, 0, [0], [1], builtin_options_type=_tfl_builtin_options.ReshapeOptions, builtin_options=reshape_opts, ) subgraph = _build_subgraph( builder, tensors=[t_in, t_ou], operators=[reshape_op], inputs=[0], outputs=[1], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.RESHAPE)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder), _build_buffer(builder)], ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 4), dtype="uint8"), ) -> R.Tensor((2, 2), dtype="uint8"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((1, 4), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(128, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((2, 2), dtype="float32") = R.reshape( lv, R.shape([2, 2]), ) gv: R.Tensor((2, 2), dtype="uint8") = R.quantize( lv1, R.const(1.0, "float32"), R.const(100, "int32"), out_dtype="uint8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_transpose_conv_with_int32_bias_dequantizes_bias(): """TRANSPOSE_CONV with INT32 bias dequantizes bias before adding.""" import struct import flatbuffers import tflite.Model builder = flatbuffers.Builder(2048) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) wt_q = _build_quantization_parameters( builder, scale=[0.25], zero_point=[0], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_in = _build_tensor( builder, 0, [1, 1, 1, 1], tensor_type=_tfl_tensor_type.INT8, quantization=in_q ) t_wt = _build_tensor( builder, 1, [1, 1, 1, 1], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q ) t_bi = _build_tensor(builder, 2, [1], tensor_type=_tfl_tensor_type.INT32) t_ou = _build_tensor( builder, 3, [1, 1, 1, 1], tensor_type=_tfl_tensor_type.INT8, quantization=out_q ) oshape_data = struct.pack(" R.Tensor((1, 1, 1, 1), dtype="int8"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((1, 1, 1, 1), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((1, 1, 1, 1), dtype="int8") = R.permute_dims( tvmgen_tensor_1, axes=[3, 0, 1, 2], ) lv2: R.Tensor((1, 1, 1, 1), dtype="float32") = R.dequantize( lv1, R.const(0.25, "float32"), R.const(0, "int32"), out_dtype="float32", axis=1, ) lv3: R.Tensor((1, 1, 1, 1), dtype="float32") = R.nn.conv2d_transpose( lv, lv2, strides=[1, 1], padding=[0, 0, 0, 0], data_layout="NHWC", kernel_layout="IOHW", out_dtype="float32", ) lv4: R.Tensor((), dtype="float32") = R.multiply( R.const(0.5, "float32"), R.const(0.25, "float32"), ) lv5: R.Tensor((1,), dtype="float32") = R.dequantize( tvmgen_tensor_2, lv4, R.const(0, "int32"), out_dtype="float32", axis=0, ) lv6: R.Tensor((1, 1, 1, 1), dtype="float32") = R.add(lv3, lv5) gv: R.Tensor((1, 1, 1, 1), dtype="int8") = R.quantize( lv6, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_quantized_fully_connected_with_int32_bias_dequantizes_bias(): """Quantized FullyConnected with INT32 bias dequantizes bias with in_scale x wt_scale.""" import flatbuffers import tflite.Model builder = flatbuffers.Builder(2048) in_q = _build_quantization_parameters( builder, scale=[0.5], zero_point=[3], quantized_dimension=0 ) wt_q = _build_quantization_parameters( builder, scale=[0.25], zero_point=[0], quantized_dimension=0 ) out_q = _build_quantization_parameters( builder, scale=[1.0], zero_point=[0], quantized_dimension=0 ) t_in = _build_tensor(builder, 0, [1, 4], tensor_type=_tfl_tensor_type.INT8, quantization=in_q) t_wt = _build_tensor(builder, 1, [2, 4], tensor_type=_tfl_tensor_type.INT8, quantization=wt_q) t_bi = _build_tensor(builder, 2, [2], tensor_type=_tfl_tensor_type.INT32) t_ou = _build_tensor(builder, 3, [1, 2], tensor_type=_tfl_tensor_type.INT8, quantization=out_q) _tfl_fully_connected_options.FullyConnectedOptionsStart(builder) _tfl_fully_connected_options.FullyConnectedOptionsAddFusedActivationFunction(builder, 0) _tfl_fully_connected_options.FullyConnectedOptionsAddWeightsFormat( builder, _tfl_fc_weights_format.DEFAULT ) _tfl_fully_connected_options.FullyConnectedOptionsAddKeepNumDims(builder, 0) fc_opts = _tfl_fully_connected_options.FullyConnectedOptionsEnd(builder) fc_op = _build_operator( builder, 0, [0, 1, 2], [3], builtin_options_type=_tfl_builtin_options.FullyConnectedOptions, builtin_options=fc_opts, ) subgraph = _build_subgraph( builder, tensors=[t_in, t_wt, t_bi, t_ou], operators=[fc_op], inputs=[0, 1, 2], outputs=[3], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.FULLY_CONNECTED)] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[_build_buffer(builder)] * 4, ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 4), dtype="int8"), tvmgen_tensor_1: R.Tensor((2, 4), dtype="int8"), tvmgen_tensor_2: R.Tensor((2,), dtype="int32"), ) -> R.Tensor((1, 2), dtype="int8"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((1, 4), dtype="float32") = R.dequantize( tvmgen_tensor_0, R.const(0.5, "float32"), R.const(3, "int32"), out_dtype="float32", axis=0, ) lv1: R.Tensor((4, 2), dtype="int8") = R.permute_dims( tvmgen_tensor_1, axes=[1, 0], ) lv2: R.Tensor((4, 2), dtype="float32") = R.dequantize( lv1, R.const(0.25, "float32"), R.const(0, "int32"), out_dtype="float32", axis=1, ) lv3: R.Tensor((1, 2), dtype="float32") = R.matmul(lv, lv2) lv4: R.Tensor((), dtype="float32") = R.multiply( R.const(0.5, "float32"), R.const(0.25, "float32"), ) lv5: R.Tensor((2,), dtype="float32") = R.dequantize( tvmgen_tensor_2, lv4, R.const(0, "int32"), out_dtype="float32", axis=0, ) lv6: R.Tensor((1, 2), dtype="float32") = R.add(lv3, lv5) gv: R.Tensor((1, 2), dtype="int8") = R.quantize( lv6, R.const(1.0, "float32"), R.const(0, "int32"), out_dtype="int8", axis=0, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_csr_sparsity( builder, *, dense_sizes, row_ptrs, col_indices, sparse_axis, traversal_order=None, ): row_ptrs_vec = _tflite_int32_table(builder, row_ptrs) col_indices_vec = _tflite_int32_table(builder, col_indices) dim_metadata = [] for axis, dense_size in enumerate(dense_sizes): _tfl_dimension_metadata.DimensionMetadataStart(builder) if axis == sparse_axis: _tfl_dimension_metadata.DimensionMetadataAddFormat( builder, _tfl_dimension_type.SPARSE_CSR ) _tfl_dimension_metadata.DimensionMetadataAddArraySegmentsType( builder, _tfl_sparse_index_vector.Int32Vector ) _tfl_dimension_metadata.DimensionMetadataAddArraySegments(builder, row_ptrs_vec) _tfl_dimension_metadata.DimensionMetadataAddArrayIndicesType( builder, _tfl_sparse_index_vector.Int32Vector ) _tfl_dimension_metadata.DimensionMetadataAddArrayIndices(builder, col_indices_vec) else: _tfl_dimension_metadata.DimensionMetadataAddFormat(builder, _tfl_dimension_type.DENSE) _tfl_dimension_metadata.DimensionMetadataAddDenseSize(builder, dense_size) dim_metadata.append(_tfl_dimension_metadata.DimensionMetadataEnd(builder)) if traversal_order is None: traversal_order = list(range(len(dense_sizes))) traversal_order_vec = _tflite_int32_vector( builder, _tfl_sparsity_parameters.SparsityParametersStartTraversalOrderVector, traversal_order, ) dim_metadata_vec = _tflite_offset_vector( builder, _tfl_sparsity_parameters.SparsityParametersStartDimMetadataVector, dim_metadata ) _tfl_sparsity_parameters.SparsityParametersStart(builder) _tfl_sparsity_parameters.SparsityParametersAddTraversalOrder(builder, traversal_order_vec) _tfl_sparsity_parameters.SparsityParametersAddDimMetadata(builder, dim_metadata_vec) return _tfl_sparsity_parameters.SparsityParametersEnd(builder) def _build_densify_only_case(builder): sparse_tensor_idx = 0 dense_tensor_idx = 1 shape = [2, 2] sparsity = _build_csr_sparsity( builder, dense_sizes=shape, row_ptrs=_DENSIFY_ROW_PTRS, col_indices=_DENSIFY_COL_INDICES, sparse_axis=1, ) sparse_tensor = _build_tensor(builder, 0, shape, sparsity) dense_tensor = _build_tensor(builder, 1, shape) densify_op = _build_operator( builder, 0, [sparse_tensor_idx], [dense_tensor_idx], _tfl_builtin_options.DensifyOptions, ) subgraph = _build_subgraph( builder, tensors=[sparse_tensor, dense_tensor], operators=[densify_op], inputs=[], outputs=[dense_tensor_idx], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.DENSIFY)] return _DENSIFY_TEST_VALUES, subgraph, operator_codes def _build_densify_add_case(builder): input_tensor_idx = 0 sparse_tensor_idx = 1 dense_tensor_idx = 2 output_tensor_idx = 3 shape = [2, 2] sparsity = _build_csr_sparsity( builder, dense_sizes=shape, row_ptrs=_DENSIFY_ROW_PTRS, col_indices=_DENSIFY_COL_INDICES, sparse_axis=1, ) input_tensor = _build_tensor(builder, 1, shape) sparse_tensor = _build_tensor(builder, 0, shape, sparsity) dense_tensor = _build_tensor(builder, 1, shape) output_tensor = _build_tensor(builder, 1, shape) densify_op = _build_operator( builder, 1, [sparse_tensor_idx], [dense_tensor_idx], _tfl_builtin_options.DensifyOptions, ) _tfl_add_options.AddOptionsStart(builder) add_options = _tfl_add_options.AddOptionsEnd(builder) add_op = _build_operator( builder, 0, [input_tensor_idx, dense_tensor_idx], [output_tensor_idx], _tfl_builtin_options.AddOptions, add_options, ) subgraph = _build_subgraph( builder, tensors=[input_tensor, sparse_tensor, dense_tensor, output_tensor], operators=[densify_op, add_op], inputs=[input_tensor_idx], outputs=[output_tensor_idx], ) operator_codes = [ _build_operator_code(builder, _tfl_builtin_operator.ADD), _build_operator_code(builder, _tfl_builtin_operator.DENSIFY), ] return _DENSIFY_TEST_VALUES, subgraph, operator_codes def _build_densify_conv2d_case(builder): input_tensor_idx = 0 sparse_kernel_idx = 1 dense_kernel_idx = 2 output_tensor_idx = 3 sparsity = _build_csr_sparsity( builder, dense_sizes=[1, 2, 2, 1], row_ptrs=_DENSIFY_ROW_PTRS, col_indices=_DENSIFY_COL_INDICES, sparse_axis=2, ) input_tensor = _build_tensor(builder, 1, [1, 4, 4, 1]) sparse_kernel = _build_tensor(builder, 0, [1, 2, 2, 1], sparsity) dense_kernel = _build_tensor(builder, 1, [1, 2, 2, 1]) output_tensor = _build_tensor(builder, 1, [1, 4, 4, 1]) _tfl_conv2d_options.Conv2DOptionsStart(builder) _tfl_conv2d_options.Conv2DOptionsAddStrideH(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddStrideW(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddPadding(builder, _tfl_padding.SAME) _tfl_conv2d_options.Conv2DOptionsAddDilationHFactor(builder, 1) _tfl_conv2d_options.Conv2DOptionsAddDilationWFactor(builder, 1) conv2d_options = _tfl_conv2d_options.Conv2DOptionsEnd(builder) densify_op = _build_operator( builder, 1, [sparse_kernel_idx], [dense_kernel_idx], _tfl_builtin_options.DensifyOptions, ) conv2d_op = _build_operator( builder, 0, [input_tensor_idx, dense_kernel_idx], [output_tensor_idx], _tfl_builtin_options.Conv2DOptions, conv2d_options, ) subgraph = _build_subgraph( builder, tensors=[input_tensor, sparse_kernel, dense_kernel, output_tensor], operators=[densify_op, conv2d_op], inputs=[input_tensor_idx], outputs=[output_tensor_idx], ) operator_codes = [ _build_operator_code(builder, _tfl_builtin_operator.CONV_2D), _build_operator_code(builder, _tfl_builtin_operator.DENSIFY), ] return _DENSIFY_TEST_VALUES, subgraph, operator_codes def _build_densify_fully_connected_case(builder): input_tensor_idx = 0 sparse_weight_idx = 1 dense_weight_idx = 2 output_tensor_idx = 3 weight_shape = [4, 4] sparsity = _build_csr_sparsity( builder, dense_sizes=weight_shape, row_ptrs=_DENSIFY_FC_ROW_PTRS, col_indices=_DENSIFY_FC_COL_INDICES, sparse_axis=1, ) input_tensor = _build_tensor(builder, 1, [1, 4]) sparse_weight = _build_tensor(builder, 0, weight_shape, sparsity) dense_weight = _build_tensor(builder, 1, weight_shape) output_tensor = _build_tensor(builder, 1, [1, 4]) _tfl_fully_connected_options.FullyConnectedOptionsStart(builder) _tfl_fully_connected_options.FullyConnectedOptionsAddWeightsFormat( builder, _tfl_fc_weights_format.DEFAULT ) fc_options = _tfl_fully_connected_options.FullyConnectedOptionsEnd(builder) densify_op = _build_operator( builder, 1, [sparse_weight_idx], [dense_weight_idx], _tfl_builtin_options.DensifyOptions, ) fc_op = _build_operator( builder, 0, [input_tensor_idx, dense_weight_idx], [output_tensor_idx], _tfl_builtin_options.FullyConnectedOptions, fc_options, ) subgraph = _build_subgraph( builder, tensors=[input_tensor, sparse_weight, dense_weight, output_tensor], operators=[densify_op, fc_op], inputs=[input_tensor_idx], outputs=[output_tensor_idx], ) operator_codes = [ _build_operator_code(builder, _tfl_builtin_operator.FULLY_CONNECTED), _build_operator_code(builder, _tfl_builtin_operator.DENSIFY), ] return _DENSIFY_FC_WEIGHT_VALUES, subgraph, operator_codes def _build_densify_model(*, downstream_op=None): """Build a sparse TFLite model with DENSIFY operator for testing.""" scenario_builders = { None: _build_densify_only_case, "add": _build_densify_add_case, "conv2d": _build_densify_conv2d_case, "fully_connected": _build_densify_fully_connected_case, } if downstream_op not in scenario_builders: raise ValueError(f"Unsupported DENSIFY downstream op: {downstream_op}") builder = flatbuffers.Builder(4096) sparse_values, subgraph, operator_codes = scenario_builders[downstream_op](builder) sparse_buffer = _build_buffer(builder, sparse_values.tobytes()) empty_buffer = _build_buffer(builder) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=[sparse_buffer, empty_buffer], ) def _load_densify_module(downstream_op=None): """Load a DENSIFY test model and return the converted Relax module.""" model_bytes = _build_densify_model(downstream_op=downstream_op) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(model_bytes, 0) else: tflite_model = tflite.Model.GetRootAsModel(model_bytes, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") return mod def test_densify(): """Test TFLite DENSIFY operator conversion.""" mod = _load_densify_module() @I.ir_module class Expected: @R.function def main() -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 0}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.const(_DENSIFY_TEST_DENSE) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_densify_with_add(): """Test DENSIFY followed by a downstream ADD operator.""" mod = _load_densify_module(downstream_op="add") @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 2), dtype="float32")) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((2, 2), dtype="float32") = R.add(x, R.const(_DENSIFY_TEST_DENSE)) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_densify_with_conv2d(): """Test DENSIFY followed by CONV2D - a real-world scenario. This simulates a sparse convolution where DENSIFY converts sparse weights before CONV2D uses them for inference. """ mod = _load_densify_module(downstream_op="conv2d") @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 4, 4, 1), dtype="float32")) -> R.Tensor( (1, 4, 4, 1), dtype="float32" ): R.func_attr({"num_input": 1}) with R.dataflow(): gv: R.Tensor((1, 4, 4, 1), dtype="float32") = R.nn.conv2d( x, R.const(_DENSIFY_CONV_KERNEL_DENSE_HWIO), strides=[1, 1], padding=[0, 0, 1, 1], dilation=[1, 1], groups=1, data_layout="NHWC", kernel_layout="HWIO", out_layout="NHWC", ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_densify_with_fully_connected(): """Test DENSIFY followed by FULLY_CONNECTED - a real-world scenario. This simulates a sparse fully connected layer where DENSIFY converts sparse weights before matrix multiplication for inference. """ mod = _load_densify_module(downstream_op="fully_connected") @I.ir_module class Expected: @R.function def main(x: R.Tensor((1, 4), dtype="float32")) -> R.Tensor((1, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): weight_t: R.Tensor((4, 4), dtype="float32") = R.permute_dims( R.const(_DENSIFY_FC_WEIGHT_DENSE_OI), axes=[1, 0] ) gv: R.Tensor((1, 4), dtype="float32") = R.matmul(x, weight_t) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def _build_dilate_only_case( builder, *, input_shape, dilations, dilation_value, dynamic_dilations=False ): input_tensor_idx = 0 dilations_tensor_idx = 1 padding_value_tensor_idx = 2 output_tensor_idx = 3 output_shape = tuple((input_shape[i] - 1) * dilations[i] + 1 for i in range(len(input_shape))) input_tensor = _build_tensor(builder, 1, input_shape) dilations_tensor = _build_tensor( builder, 2, [len(dilations)], tensor_type=_tfl_tensor_type.INT32 ) padding_value_tensor = _build_tensor(builder, 3, []) output_tensor = _build_tensor(builder, 4, output_shape) _tfl_dilate_options.DilateOptionsStart(builder) dilate_opts = _tfl_dilate_options.DilateOptionsEnd(builder) dilate_op = _build_operator( builder, 0, [input_tensor_idx, dilations_tensor_idx, padding_value_tensor_idx], [output_tensor_idx], builtin_options2_type=_tfl_builtin_options2.DilateOptions, builtin_options2=dilate_opts, ) sg_inputs = ( [input_tensor_idx, dilations_tensor_idx] if dynamic_dilations else [input_tensor_idx] ) subgraph = _build_subgraph( builder, tensors=[input_tensor, dilations_tensor, padding_value_tensor, output_tensor], operators=[dilate_op], inputs=sg_inputs, outputs=[output_tensor_idx], ) operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.DILATE)] return subgraph, operator_codes def test_dilate(): """TFLite DILATE with constant dilations""" builder = flatbuffers.Builder(1024) input_shape = (3, 4) dilations = [2, 2] dilation_value = 0.5 subgraph, operator_codes = _build_dilate_only_case( builder, input_shape=input_shape, dilations=dilations, dilation_value=dilation_value, ) buffers = [ _build_buffer(builder), _build_buffer(builder), _build_buffer(builder, np.asarray(dilations, dtype=np.int32).tobytes()), _build_buffer(builder, np.asarray([dilation_value], dtype=np.float32).tobytes()), _build_buffer(builder), ] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=buffers ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((3, 4), dtype="float32"), ) -> R.Tensor((5, 7), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((3, 1, 4), dtype="float32") = R.reshape( tvmgen_tensor_0, R.shape([3, 1, 4]) ) lv1: R.Tensor((3, 1, 4), dtype="float32") = R.full( R.shape([3, 1, 4]), R.const(0.5, "float32"), dtype="float32" ) lv2: R.Tensor((3, 2, 4), dtype="float32") = R.concat((lv, lv1), axis=1) lv3: R.Tensor((6, 4), dtype="float32") = R.reshape(lv2, R.shape([6, 4])) lv4: R.Tensor((5, 4), dtype="float32") = R.strided_slice( lv3, [0, 1], [0, 0], [5, 4], [1, 1], assume_inbound=False ) lv5: R.Tensor((5, 4, 1), dtype="float32") = R.reshape(lv4, R.shape([5, 4, 1])) lv6: R.Tensor((5, 4, 1), dtype="float32") = R.full( R.shape([5, 4, 1]), R.const(0.5, "float32"), dtype="float32" ) lv7: R.Tensor((5, 4, 2), dtype="float32") = R.concat((lv5, lv6), axis=2) lv8: R.Tensor((5, 8), dtype="float32") = R.reshape(lv7, R.shape([5, 8])) gv: R.Tensor((5, 7), dtype="float32") = R.strided_slice( lv8, [0, 1], [0, 0], [5, 7], [1, 1], assume_inbound=False ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_dilate_dynamic_dilations(): """DILATE with runtime dilations""" builder = flatbuffers.Builder(1024) input_shape = (3, 4) dilations_for_shape = [2, 2] dilation_value = 0.5 subgraph, operator_codes = _build_dilate_only_case( builder, input_shape=input_shape, dilations=dilations_for_shape, dilation_value=dilation_value, dynamic_dilations=True, ) buffers = [ _build_buffer(builder), _build_buffer(builder), _build_buffer(builder), # dilations is a runtime input so empty buffer _build_buffer(builder, np.asarray([dilation_value], dtype=np.float32).tobytes()), _build_buffer(builder), ] buf = _finish_tflite_model( builder, subgraph=subgraph, operator_codes=operator_codes, buffers=buffers ) if hasattr(tflite.Model, "Model"): tflite_model = tflite.Model.Model.GetRootAsModel(buf, 0) else: tflite_model = tflite.Model.GetRootAsModel(buf, 0) mod = from_tflite(tflite_model) mod["main"] = mod["main"].without_attr("params") @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((3, 4), dtype="float32"), tvmgen_tensor_1: R.Tensor((2,), dtype="int32"), ) -> R.Tensor(dtype="float32", ndim=2): R.func_attr({"num_input": 2}) dilate_stride_0 = T.int64() dilate_stride_1 = T.int64() with R.dataflow(): lv: R.Tensor((2,), dtype="int32") = R.match_cast( tvmgen_tensor_1, R.Tensor((2,), dtype="int32") ) lv1: R.Tensor((2,), dtype="int64") = R.astype(lv, dtype="int64") lv2: R.Shape(ndim=2) = R.tensor_to_shape(lv1) _lv3: R.Shape([dilate_stride_0, dilate_stride_1]) = R.match_cast( lv2, R.Shape([dilate_stride_0, dilate_stride_1]) ) lv4: R.Tensor((3, 1, 4), dtype="float32") = R.reshape( tvmgen_tensor_0, R.shape([3, 1, 4]) ) lv5: R.Tensor((3, dilate_stride_0 - 1, 4), dtype="float32") = R.full( R.shape([3, dilate_stride_0 - 1, 4]), R.const(0.5, "float32"), dtype="float32", ) lv6: R.Tensor((3, 1 + (dilate_stride_0 - 1), 4), dtype="float32") = R.concat( (lv4, lv5), axis=1 ) lv7: R.Tensor((3 * dilate_stride_0, 4), dtype="float32") = R.reshape( lv6, R.shape([3 * dilate_stride_0, 4]) ) lv8: R.Tensor( (T.min(dilate_stride_0 * 2 + 1, dilate_stride_0 * 3), 4), dtype="float32", ) = R.strided_slice( lv7, [0, 1], [0, 0], [2 * dilate_stride_0 + 1, 4], [1, 1], assume_inbound=False, ) lv9: R.Tensor((2 * dilate_stride_0 + 1, 4, 1), dtype="float32") = R.reshape( lv8, R.shape([2 * dilate_stride_0 + 1, 4, 1]) ) lv10: R.Tensor( (2 * dilate_stride_0 + 1, 4, dilate_stride_1 - 1), dtype="float32" ) = R.full( R.shape([2 * dilate_stride_0 + 1, 4, dilate_stride_1 - 1]), R.const(0.5, "float32"), dtype="float32", ) lv11: R.Tensor( (2 * dilate_stride_0 + 1, 4, 1 + (dilate_stride_1 - 1)), dtype="float32", ) = R.concat((lv9, lv10), axis=2) lv12: R.Tensor((2 * dilate_stride_0 + 1, 4 * dilate_stride_1), dtype="float32") = ( R.reshape(lv11, R.shape([2 * dilate_stride_0 + 1, 4 * dilate_stride_1])) ) gv: R.Tensor( ( dilate_stride_0 * 2 + 1, T.min(dilate_stride_1 * 3 + 1, dilate_stride_1 * 4), ), dtype="float32", ) = R.strided_slice( lv12, [0, 1], [0, 0], [2 * dilate_stride_0 + 1, 3 * dilate_stride_1 + 1], [1, 1], assume_inbound=False, ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) # ── LSTM ────────────────────────────────────────────────────────────────────── def _build_lstm_model( batch, input_size, num_units, input_to_forget_weights, input_to_cell_weights, input_to_output_weights, recurrent_to_forget_weights, recurrent_to_cell_weights, recurrent_to_output_weights, forget_gate_bias, cell_bias, output_gate_bias, activation, *, cell_clip=0.0, proj_clip=0.0, include_unsupported=False, ): """Build a minimal TFLite flatbuffer model with one LSTM op (coupled input-forget). Tensor indices: 0 - input [batch, input_size] 1 - input_to_forget_weights [num_units, input_size] (constant) 2 - input_to_cell_weights [num_units, input_size] (constant) 3 - input_to_output_weights [num_units, input_size] (constant) 4 - recurrent_to_forget_weights [num_units, num_units] (constant) 5 - recurrent_to_cell_weights [num_units, num_units] (constant) 6 - recurrent_to_output_weights [num_units, num_units] (constant) 7 - forget_gate_bias [num_units] (constant) 8 - cell_bias [num_units] (constant) 9 - output_gate_bias [num_units] (constant) 10 - output_state [batch, num_units] (input) 11 - cell_state [batch, num_units] (input) 12 - output [batch, num_units] Operator input indices (24 entries, -1 for absent): [0, -1, 1, 2, 3, -1, 4, 5, 6, -1, -1, -1, -1, 7, 8, 9, -1, -1, 10, 11, -1, -1, -1, -1] """ builder = flatbuffers.Builder(4096) _tfl_lstm_options.LSTMOptionsStart(builder) _tfl_lstm_options.LSTMOptionsAddFusedActivationFunction(builder, activation) _tfl_lstm_options.LSTMOptionsAddCellClip(builder, cell_clip) _tfl_lstm_options.LSTMOptionsAddProjClip(builder, proj_clip) lstm_opts = _tfl_lstm_options.LSTMOptionsEnd(builder) lstm_op_code = _build_operator_code(builder, _tfl_builtin_operator.LSTM) def _t(buf_idx, shape): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, False) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) tensors = [ # 0: input _t(0, [batch, input_size]), # 1: input_to_forget_weights (coupled) _t(1, [num_units, input_size]), # 2: input_to_cell_weights _t(2, [num_units, input_size]), # 3: input_to_output_weights _t(3, [num_units, input_size]), # 4: recurrent_to_forget_weights (coupled) _t(4, [num_units, num_units]), # 5: recurrent_to_cell_weights _t(5, [num_units, num_units]), # 6: recurrent_to_output_weights _t(6, [num_units, num_units]), # 7: forget_gate_bias (coupled) _t(7, [num_units]), # 8: cell_bias _t(8, [num_units]), # 9: output_gate_bias _t(9, [num_units]), # 10: output_state (input) _t(0, [batch, num_units]), # 11: cell_state (input) _t(0, [batch, num_units]), # 12: output _t(0, [batch, num_units]), ] if include_unsupported: tensors.extend( [ _t(0, [num_units]), _t(0, [num_units]), _t(0, [num_units]), _t(0, [num_units, num_units]), _t(0, [num_units]), _t(0, [num_units]), _t(0, [num_units]), _t(0, [num_units]), _t(0, [num_units]), ] ) # Operator input indices: -1 for absent optional inputs lstm_inputs = [ 0, -1, 1, 2, 3, -1, 4, 5, 6, 13 if include_unsupported else -1, 14 if include_unsupported else -1, 15 if include_unsupported else -1, -1, 7, 8, 9, 16 if include_unsupported else -1, 17 if include_unsupported else -1, 10, 11, 18 if include_unsupported else -1, 19 if include_unsupported else -1, 20 if include_unsupported else -1, 21 if include_unsupported else -1, ] lstm_op = _build_operator( builder, 0, lstm_inputs, [12], builtin_options_type=_tfl_builtin_options.LSTMOptions, builtin_options=lstm_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[lstm_op], inputs=[0, 10, 11], outputs=[12], ) buffers = [ _build_buffer(builder), # 0: empty _build_buffer(builder, input_to_forget_weights.tobytes()), # 1 _build_buffer(builder, input_to_cell_weights.tobytes()), # 2 _build_buffer(builder, input_to_output_weights.tobytes()), # 3 _build_buffer(builder, recurrent_to_forget_weights.tobytes()), # 4 _build_buffer(builder, recurrent_to_cell_weights.tobytes()), # 5 _build_buffer(builder, recurrent_to_output_weights.tobytes()), # 6 _build_buffer(builder, forget_gate_bias.tobytes()), # 7 _build_buffer(builder, cell_bias.tobytes()), # 8 _build_buffer(builder, output_gate_bias.tobytes()), # 9 ] if include_unsupported: buffers.extend([_build_buffer(builder) for _ in range(9)]) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[lstm_op_code], buffers=buffers, ) def test_lstm_none_activation(): """LSTM with NONE activation uses the cell state before the output gate multiply.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, input_size, num_units = 2, 2, 2 w_f = np.eye(num_units, input_size, dtype=np.float32) w_c = np.eye(num_units, input_size, dtype=np.float32) w_o = np.eye(num_units, input_size, dtype=np.float32) r_f = np.eye(num_units, dtype=np.float32) r_c = np.eye(num_units, dtype=np.float32) r_o = np.eye(num_units, dtype=np.float32) b_f = np.zeros(num_units, dtype=np.float32) b_c = np.zeros(num_units, dtype=np.float32) b_o = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_lstm_model( batch, input_size, num_units, w_f, w_c, w_o, r_f, r_c, r_o, b_f, b_c, b_o, ActivationFunctionType.NONE, ) ) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), tvmgen_tensor_10: R.Tensor((2, 2), dtype="float32"), tvmgen_tensor_11: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv1: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_0, lv) lv2: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv3: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_10, lv2) lv4: R.Tensor((2, 2), dtype="float32") = R.add(lv1, lv3) lv5: R.Tensor((2, 2), dtype="float32") = R.add( lv4, R.const(np.zeros(2, dtype=np.float32)) ) lv6: R.Tensor((2, 2), dtype="float32") = R.sigmoid(lv5) lv7: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv8: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_0, lv7) lv9: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv10: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_10, lv9) lv11: R.Tensor((2, 2), dtype="float32") = R.add(lv8, lv10) lv12: R.Tensor((2, 2), dtype="float32") = R.add( lv11, R.const(np.zeros(2, dtype=np.float32)) ) lv13: R.Tensor((2, 2), dtype="float32") = R.sigmoid(lv12) lv14: R.Tensor((2, 2), dtype="float32") = R.multiply(lv13, tvmgen_tensor_11) lv15: R.Tensor((2, 2), dtype="float32") = R.subtract(R.const(1.0, "float32"), lv13) lv16: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv17: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_0, lv16) lv18: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv19: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_10, lv18) lv20: R.Tensor((2, 2), dtype="float32") = R.add(lv17, lv19) lv21: R.Tensor((2, 2), dtype="float32") = R.add( lv20, R.const(np.zeros(2, dtype=np.float32)) ) lv22: R.Tensor((2, 2), dtype="float32") = R.tanh(lv21) lv23: R.Tensor((2, 2), dtype="float32") = R.multiply(lv15, lv22) lv24: R.Tensor((2, 2), dtype="float32") = R.add(lv14, lv23) gv: R.Tensor((2, 2), dtype="float32") = R.multiply(lv6, lv24) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_lstm_tanh_activation(): """LSTM with TANH activation applies tanh before the output gate multiply.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, input_size, num_units = 2, 2, 2 w_f = np.eye(num_units, input_size, dtype=np.float32) w_c = np.eye(num_units, input_size, dtype=np.float32) w_o = np.eye(num_units, input_size, dtype=np.float32) r_f = np.eye(num_units, dtype=np.float32) r_c = np.eye(num_units, dtype=np.float32) r_o = np.eye(num_units, dtype=np.float32) b_f = np.zeros(num_units, dtype=np.float32) b_c = np.zeros(num_units, dtype=np.float32) b_o = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_lstm_model( batch, input_size, num_units, w_f, w_c, w_o, r_f, r_c, r_o, b_f, b_c, b_o, ActivationFunctionType.TANH, ) ) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((2, 2), dtype="float32"), tvmgen_tensor_10: R.Tensor((2, 2), dtype="float32"), tvmgen_tensor_11: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 2), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv1: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_0, lv) lv2: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv3: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_10, lv2) lv4: R.Tensor((2, 2), dtype="float32") = R.add(lv1, lv3) lv5: R.Tensor((2, 2), dtype="float32") = R.add( lv4, R.const(np.zeros(2, dtype=np.float32)) ) lv6: R.Tensor((2, 2), dtype="float32") = R.sigmoid(lv5) lv7: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv8: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_0, lv7) lv9: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv10: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_10, lv9) lv11: R.Tensor((2, 2), dtype="float32") = R.add(lv8, lv10) lv12: R.Tensor((2, 2), dtype="float32") = R.add( lv11, R.const(np.zeros(2, dtype=np.float32)) ) lv13: R.Tensor((2, 2), dtype="float32") = R.sigmoid(lv12) lv14: R.Tensor((2, 2), dtype="float32") = R.multiply(lv13, tvmgen_tensor_11) lv15: R.Tensor((2, 2), dtype="float32") = R.subtract(R.const(1.0, "float32"), lv13) lv16: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv17: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_0, lv16) lv18: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv19: R.Tensor((2, 2), dtype="float32") = R.matmul(tvmgen_tensor_10, lv18) lv20: R.Tensor((2, 2), dtype="float32") = R.add(lv17, lv19) lv21: R.Tensor((2, 2), dtype="float32") = R.add( lv20, R.const(np.zeros(2, dtype=np.float32)) ) lv22: R.Tensor((2, 2), dtype="float32") = R.tanh(lv21) lv23: R.Tensor((2, 2), dtype="float32") = R.multiply(lv15, lv22) lv24: R.Tensor((2, 2), dtype="float32") = R.add(lv14, lv23) lv25: R.Tensor((2, 2), dtype="float32") = R.tanh(lv24) gv: R.Tensor((2, 2), dtype="float32") = R.multiply(lv6, lv25) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_lstm_rejects_unsupported_features(): """LSTM with peephole/projection/layer norm tensors should be rejected.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, input_size, num_units = 2, 2, 2 zeros_w = np.zeros((num_units, input_size), dtype=np.float32) zeros_r = np.zeros((num_units, num_units), dtype=np.float32) zeros_b = np.zeros(num_units, dtype=np.float32) with pytest.raises(tvm.error.OpNotImplemented, match="not supported yet"): _load_model_from_buffer( _build_lstm_model( batch, input_size, num_units, zeros_w, zeros_w, zeros_w, zeros_r, zeros_r, zeros_r, zeros_b, zeros_b, zeros_b, ActivationFunctionType.NONE, include_unsupported=True, ) ) # ── SVDF ────────────────────────────────────────────────────────────────────── def _build_svdf_model( batch, input_size, num_units, rank, memory_size, num_filters, feat_weights, time_weights, bias, activation, ): """Build a minimal TFLite flatbuffer model containing one SVDF op. Tensor indices: 0 - input [batch, input_size] (model input) 1 - feature_weights [num_filters, input_size] (constant) 2 - time_weights [num_filters, memory_size] (constant) 3 - bias [num_units] (constant) 4 - state [batch, num_filters * memory_size] (variable, model input) 5 - output [batch, num_units] """ builder = flatbuffers.Builder(4096) _tfl_svdf_options.SVDFOptionsStart(builder) _tfl_svdf_options.SVDFOptionsAddRank(builder, rank) _tfl_svdf_options.SVDFOptionsAddFusedActivationFunction(builder, activation) svdf_opts = _tfl_svdf_options.SVDFOptionsEnd(builder) svdf_op_code = _build_operator_code(builder, _tfl_builtin_operator.SVDF) def _t(buf_idx, shape): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, False) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) tensors = [ _t(0, [batch, input_size]), # 0: input _t(1, [num_filters, input_size]), # 1: feature_weights _t(2, [num_filters, memory_size]), # 2: time_weights _t(3, [num_units]), # 3: bias _t(0, [batch, num_filters * memory_size]), # 4: state (variable, zero-filled) _t(0, [batch, num_units]), # 5: output ] svdf_op = _build_operator( builder, 0, [0, 1, 2, 3, 4], [5], builtin_options_type=_tfl_builtin_options.SVDFOptions, builtin_options=svdf_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[svdf_op], inputs=[0, 4], outputs=[5], ) buffers = [ _build_buffer(builder), # 0: empty _build_buffer(builder, feat_weights.tobytes()), # 1 _build_buffer(builder, time_weights.tobytes()), # 2 _build_buffer(builder, bias.tobytes()), # 3 ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[svdf_op_code], buffers=buffers, ) def test_svdf_none_activation(): """SVDF with NONE activation, verifying output shape and params.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, input_size, num_units, rank, memory_size = 2, 3, 2, 2, 3 num_filters = num_units * rank np.random.seed(42) feat_weights = np.random.randn(num_filters, input_size).astype(np.float32) time_weights = np.random.randn(num_filters, memory_size).astype(np.float32) bias = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_svdf_model( batch, input_size, num_units, rank, memory_size, num_filters, feat_weights, time_weights, bias, ActivationFunctionType.NONE, ) ) fn = mod["main"] assert len(fn.params) == 2, f"expected 2 params (input, state), got {len(fn.params)}" tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((batch, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.params[1].ty, relax.TensorType((batch, num_filters * memory_size), "float32"), ) tvm.ir.assert_structural_equal(fn.ret_ty, relax.TensorType((batch, num_units), "float32")) def _build_two_step_shared_state_svdf_model( batch, input_size, num_units, rank, memory_size, feat_weights_0, time_weights_0, bias_0, feat_weights_1, time_weights_1, bias_1, activation, ): """Build two consecutive SVDF ops sharing a single state tensor.""" builder = flatbuffers.Builder(4096) num_filters = num_units * rank _tfl_svdf_options.SVDFOptionsStart(builder) _tfl_svdf_options.SVDFOptionsAddRank(builder, rank) _tfl_svdf_options.SVDFOptionsAddFusedActivationFunction(builder, activation) svdf_opts = _tfl_svdf_options.SVDFOptionsEnd(builder) svdf_op_code = _build_operator_code(builder, _tfl_builtin_operator.SVDF) def _t(buf_idx, shape): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, False) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) tensors = [ _t(0, [batch, input_size]), # 0 input_0 _t(1, [num_filters, input_size]), # 1 feat_weights_0 _t(2, [num_filters, memory_size]), # 2 time_weights_0 _t(3, [num_units]), # 3 bias_0 _t(0, [batch, num_filters * memory_size]), # 4 shared state _t(0, [batch, num_units]), # 5 output_0 _t(0, [batch, input_size]), # 6 input_1 _t(4, [num_filters, input_size]), # 7 feat_weights_1 _t(5, [num_filters, memory_size]), # 8 time_weights_1 _t(6, [num_units]), # 9 bias_1 _t(0, [batch, num_units]), # 10 output_1 ] svdf_op_0 = _build_operator( builder, 0, [0, 1, 2, 3, 4], [5], builtin_options_type=_tfl_builtin_options.SVDFOptions, builtin_options=svdf_opts, ) svdf_op_1 = _build_operator( builder, 0, [6, 7, 8, 9, 4], [10], builtin_options_type=_tfl_builtin_options.SVDFOptions, builtin_options=svdf_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[svdf_op_0, svdf_op_1], inputs=[0, 6, 4], outputs=[10], ) buffers = [ _build_buffer(builder), _build_buffer(builder, feat_weights_0.tobytes()), _build_buffer(builder, time_weights_0.tobytes()), _build_buffer(builder, bias_0.tobytes()), _build_buffer(builder, feat_weights_1.tobytes()), _build_buffer(builder, time_weights_1.tobytes()), _build_buffer(builder, bias_1.tobytes()), ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[svdf_op_code], buffers=buffers, ) def test_svdf_shared_state_updates_exp_tab(): """Two SVDF ops sharing state should use the updated FIFO state in the second step.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, input_size, num_units, rank, memory_size = 1, 1, 1, 2, 3 feat_weights_0 = np.array([[1.0], [2.0]], dtype=np.float32) time_weights_0 = np.array([[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]], dtype=np.float32) bias_0 = np.zeros(num_units, dtype=np.float32) feat_weights_1 = np.array([[7.0], [11.0]], dtype=np.float32) time_weights_1 = np.array([[13.0, 17.0, 19.0], [23.0, 29.0, 31.0]], dtype=np.float32) bias_1 = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_two_step_shared_state_svdf_model( batch, input_size, num_units, rank, memory_size, feat_weights_0, time_weights_0, bias_0, feat_weights_1, time_weights_1, bias_1, ActivationFunctionType.NONE, ) ) @I.ir_module class Expected: @R.function def main( tvmgen_tensor_0: R.Tensor((1, 1), dtype="float32"), tvmgen_tensor_6: R.Tensor((1, 1), dtype="float32"), tvmgen_tensor_4: R.Tensor((1, 6), dtype="float32"), ) -> R.Tensor((1, 1), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): lv: R.Tensor((1, 2, 3), dtype="float32") = R.reshape( tvmgen_tensor_4, R.shape([1, 2, 3]) ) lv1: R.Tensor((1, 2, 3), dtype="float32") = R.reshape( R.const(np.array([[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]], dtype=np.float32)), R.shape([1, 2, 3]), ) lv2: R.Tensor((1, 2, 3), dtype="float32") = R.multiply(lv, lv1) lv3: R.Tensor((1, 2), dtype="float32") = R.sum(lv2, axis=[-1], keepdims=False) lv4: R.Tensor((1, 1, 2), dtype="float32") = R.reshape(lv3, R.shape([1, 1, 2])) lv5: R.Tensor((1, 1), dtype="float32") = R.sum( # noqa: F841 lv4, axis=[-1], keepdims=False ) lv6: R.Tensor((1, 2, 2), dtype="float32") = R.strided_slice( lv, (R.prim_value(2),), (R.prim_value(1),), (R.prim_value(3),), assume_inbound=False, ) lv7: R.Tensor((1, 2), dtype="float32") = R.permute_dims( R.const(np.array([[1.0], [2.0]], dtype=np.float32)), axes=None ) lv8: R.Tensor((1, 2), dtype="float32") = R.matmul( tvmgen_tensor_0, lv7, ) lv9: R.Tensor((1, 2, 1), dtype="float32") = R.expand_dims(lv8, axis=[-1]) lv10: R.Tensor((1, 2, 3), dtype="float32") = R.concat((lv6, lv9), axis=2) lv11: R.Tensor((1, 6), dtype="float32") = R.reshape(lv10, R.shape([1, 6])) lv12: R.Tensor((1, 2, 3), dtype="float32") = R.reshape(lv11, R.shape([1, 2, 3])) lv13: R.Tensor((1, 2, 3), dtype="float32") = R.reshape( R.const(np.array([[13.0, 17.0, 19.0], [23.0, 29.0, 31.0]], dtype=np.float32)), R.shape([1, 2, 3]), ) lv14: R.Tensor((1, 2, 3), dtype="float32") = R.multiply(lv12, lv13) lv15: R.Tensor((1, 2), dtype="float32") = R.sum(lv14, axis=[-1], keepdims=False) lv16: R.Tensor((1, 1, 2), dtype="float32") = R.reshape(lv15, R.shape([1, 1, 2])) lv17: R.Tensor((1, 1), dtype="float32") = R.sum(lv16, axis=[-1], keepdims=False) gv: R.Tensor((1, 1), dtype="float32") = R.add( lv17, R.const(np.zeros(1, dtype=np.float32)) ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) # ── UNIDIRECTIONAL_SEQUENCE_LSTM ───────────────────────────────────────────── def _build_unidirectional_sequence_lstm_model( batch, time, input_size, num_units, input_to_forget_weights, input_to_cell_weights, input_to_output_weights, recurrent_to_forget_weights, recurrent_to_cell_weights, recurrent_to_output_weights, forget_gate_bias, cell_bias, output_gate_bias, activation, *, time_major=False, cell_clip=0.0, proj_clip=0.0, projection_weights=None, ): """Build a TFLite flatbuffer model with one UNIDIRECTIONAL_SEQUENCE_LSTM op. Tensor indices (same layout as single-step LSTM, but input is 3D): 0 - input [batch, time, input_size] 1 - input_to_forget_weights [num_units, input_size] 2 - input_to_cell_weights [num_units, input_size] 3 - input_to_output_weights [num_units, input_size] 4 - recurrent_to_forget_weights [num_units, num_units] 5 - recurrent_to_cell_weights [num_units, num_units] 6 - recurrent_to_output_weights [num_units, num_units] 7 - forget_gate_bias [num_units] 8 - cell_bias [num_units] 9 - output_gate_bias [num_units] 10 - output_state [batch, num_units] (model input) 11 - cell_state [batch, num_units] (model input) 12 - output [batch, time, num_units] or [time, batch, num_units] """ builder = flatbuffers.Builder(4096) _tfl_unidirectional_sequence_lstm_options.UnidirectionalSequenceLSTMOptionsStart(builder) _tfl_unidirectional_sequence_lstm_options.UnidirectionalSequenceLSTMOptionsAddFusedActivationFunction( builder, activation ) _tfl_unidirectional_sequence_lstm_options.UnidirectionalSequenceLSTMOptionsAddTimeMajor( builder, time_major ) _tfl_unidirectional_sequence_lstm_options.UnidirectionalSequenceLSTMOptionsAddCellClip( builder, cell_clip ) _tfl_unidirectional_sequence_lstm_options.UnidirectionalSequenceLSTMOptionsAddProjClip( builder, proj_clip ) lstm_opts = _tfl_unidirectional_sequence_lstm_options.UnidirectionalSequenceLSTMOptionsEnd( builder ) lstm_op_code = _build_operator_code(builder, _tfl_builtin_operator.UNIDIRECTIONAL_SEQUENCE_LSTM) def _t(buf_idx, shape): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, False) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) input_shape = [time, batch, input_size] if time_major else [batch, time, input_size] output_shape = [time, batch, num_units] if time_major else [batch, time, num_units] tensors = [ _t(0, input_shape), # 0: input _t(1, [num_units, input_size]), # 1: input_to_forget_weights _t(2, [num_units, input_size]), # 2: input_to_cell_weights _t(3, [num_units, input_size]), # 3: input_to_output_weights _t(4, [num_units, num_units]), # 4: recurrent_to_forget_weights _t(5, [num_units, num_units]), # 5: recurrent_to_cell_weights _t(6, [num_units, num_units]), # 6: recurrent_to_output_weights _t(7, [num_units]), # 7: forget_gate_bias _t(8, [num_units]), # 8: cell_bias _t(9, [num_units]), # 9: output_gate_bias _t(0, [batch, num_units]), # 10: output_state (model input) _t(0, [batch, num_units]), # 11: cell_state (model input) _t(0, output_shape), # 12: output ] # 24 operator inputs, -1 for absent. lstm_inputs = [ 0, -1, 1, 2, 3, -1, 4, 5, 6, -1, -1, -1, -1, 7, 8, 9, -1, -1, 10, 11, -1, -1, -1, -1, ] buffers = [ _build_buffer(builder), # 0: empty _build_buffer(builder, input_to_forget_weights.tobytes()), # 1 _build_buffer(builder, input_to_cell_weights.tobytes()), # 2 _build_buffer(builder, input_to_output_weights.tobytes()), # 3 _build_buffer(builder, recurrent_to_forget_weights.tobytes()), # 4 _build_buffer(builder, recurrent_to_cell_weights.tobytes()), # 5 _build_buffer(builder, recurrent_to_output_weights.tobytes()), # 6 _build_buffer(builder, forget_gate_bias.tobytes()), # 7 _build_buffer(builder, cell_bias.tobytes()), # 8 _build_buffer(builder, output_gate_bias.tobytes()), # 9 ] if projection_weights is not None: tensors.append(_t(len(buffers), [num_units, num_units])) lstm_inputs[16] = len(tensors) - 1 buffers.append(_build_buffer(builder, projection_weights.tobytes())) lstm_op = _build_operator( builder, 0, lstm_inputs, [12], builtin_options_type=_tfl_builtin_options.UnidirectionalSequenceLSTMOptions, builtin_options=lstm_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[lstm_op], inputs=[0, 10, 11], outputs=[12], ) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[lstm_op_code], buffers=buffers, ) def test_unidirectional_sequence_lstm_none_activation(): """UNIDIRECTIONAL_SEQUENCE_LSTM with NONE activation keeps cell activation linear.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 1, 2, 2 w_f = np.eye(num_units, input_size, dtype=np.float32) w_c = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32) w_o = np.array([[0.5, -0.25], [0.75, 0.5]], dtype=np.float32) r_f = np.eye(num_units, dtype=np.float32) r_c = np.array([[0.5, 0.0], [0.0, 0.25]], dtype=np.float32) r_o = np.array([[0.1, 0.0], [0.0, 0.2]], dtype=np.float32) b_f = np.zeros(num_units, dtype=np.float32) b_c = np.zeros(num_units, dtype=np.float32) b_o = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_unidirectional_sequence_lstm_model( batch, time, input_size, num_units, w_f, w_c, w_o, r_f, r_c, r_o, b_f, b_c, b_o, ActivationFunctionType.NONE, ) ) fn = mod["main"] tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((batch, time, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((batch, time, num_units), "float32"), ) def test_unidirectional_sequence_lstm_tanh_activation(): """UNIDIRECTIONAL_SEQUENCE_LSTM with TANH activation applies it inside the cell.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 1, 2, 2 w_f = np.eye(num_units, input_size, dtype=np.float32) w_c = np.array([[1.0, -1.0], [0.25, 0.5]], dtype=np.float32) w_o = np.array([[0.5, 0.5], [-0.5, 1.0]], dtype=np.float32) r_f = np.eye(num_units, dtype=np.float32) r_c = np.array([[0.0, 0.1], [0.2, 0.0]], dtype=np.float32) r_o = np.array([[0.3, 0.0], [0.0, 0.4]], dtype=np.float32) b_f = np.zeros(num_units, dtype=np.float32) b_c = np.zeros(num_units, dtype=np.float32) b_o = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_unidirectional_sequence_lstm_model( batch, time, input_size, num_units, w_f, w_c, w_o, r_f, r_c, r_o, b_f, b_c, b_o, ActivationFunctionType.TANH, ) ) fn = mod["main"] tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((batch, time, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((batch, time, num_units), "float32"), ) def test_unidirectional_sequence_lstm_time_major(): """UNIDIRECTIONAL_SEQUENCE_LSTM preserves time-major output layout.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 3, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_unidirectional_sequence_lstm_model( batch, time, input_size, num_units, weights, weights, weights, recurrent, recurrent, recurrent, bias, bias, bias, ActivationFunctionType.NONE, time_major=True, ) ) fn = mod["main"] tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((time, batch, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((time, batch, num_units), "float32"), ) def test_unidirectional_sequence_lstm_rejects_projection(): """UNIDIRECTIONAL_SEQUENCE_LSTM rejects unsupported projection inputs.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 2, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) with pytest.raises(tvm.error.OpNotImplemented, match="projection LSTM"): _load_model_from_buffer( _build_unidirectional_sequence_lstm_model( batch, time, input_size, num_units, weights, weights, weights, recurrent, recurrent, recurrent, bias, bias, bias, ActivationFunctionType.NONE, projection_weights=np.eye(num_units, dtype=np.float32), ) ) # ── BIDIRECTIONAL_SEQUENCE_RNN ─────────────────────────────────────────────── def _build_bidirectional_sequence_rnn_model( batch, time, input_size, num_units, fw_weights, fw_recurrent_weights, fw_bias, bw_weights, bw_recurrent_weights, bw_bias, activation, *, time_major=False, merge_outputs=True, with_aux_input=False, ): """Build a TFLite flatbuffer model with one BIDIRECTIONAL_SEQUENCE_RNN op. Tensor indices: 0 - input [batch, time, input_size] 1 - fw_weights [num_units, input_size] 2 - fw_recurrent_weights [num_units, num_units] 3 - fw_bias [num_units] 4 - fw_hidden_state [batch, num_units] (model input) 5 - bw_weights [num_units, input_size] 6 - bw_recurrent_weights [num_units, num_units] 7 - bw_bias [num_units] 8 - bw_hidden_state [batch, num_units] (model input) 9 - aux_input (optional) 10 - fw_aux_weights (optional) 11 - bw_aux_weights (optional) 12 - output (or fw_output if merge_outputs=False) 13 - bw_output (only if merge_outputs=False) """ builder = flatbuffers.Builder(4096) _tfl_bidirectional_sequence_rnn_options.BidirectionalSequenceRNNOptionsStart(builder) _tfl_bidirectional_sequence_rnn_options.BidirectionalSequenceRNNOptionsAddTimeMajor( builder, time_major ) _tfl_bidirectional_sequence_rnn_options.BidirectionalSequenceRNNOptionsAddFusedActivationFunction( builder, activation ) _tfl_bidirectional_sequence_rnn_options.BidirectionalSequenceRNNOptionsAddMergeOutputs( builder, merge_outputs ) rnn_opts = _tfl_bidirectional_sequence_rnn_options.BidirectionalSequenceRNNOptionsEnd(builder) rnn_op_code = _build_operator_code(builder, _tfl_builtin_operator.BIDIRECTIONAL_SEQUENCE_RNN) def _t(buf_idx, shape): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, False) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) input_shape = [time, batch, input_size] if time_major else [batch, time, input_size] output_prefix = [time, batch] if time_major else [batch, time] output_shape = output_prefix + ([num_units * 2] if merge_outputs else [num_units]) tensors = [ _t(0, input_shape), # 0: input _t(1, [num_units, input_size]), # 1: fw_weights _t(2, [num_units, num_units]), # 2: fw_recurrent_weights _t(3, [num_units]), # 3: fw_bias _t(0, [batch, num_units]), # 4: fw_hidden_state (model input) _t(4, [num_units, input_size]), # 5: bw_weights _t(5, [num_units, num_units]), # 6: bw_recurrent_weights _t(6, [num_units]), # 7: bw_bias _t(0, [batch, num_units]), # 8: bw_hidden_state (model input) ] buffers = [ _build_buffer(builder), # 0: empty _build_buffer(builder, fw_weights.tobytes()), # 1 _build_buffer(builder, fw_recurrent_weights.tobytes()), # 2 _build_buffer(builder, fw_bias.tobytes()), # 3 _build_buffer(builder, bw_weights.tobytes()), # 4 _build_buffer(builder, bw_recurrent_weights.tobytes()), # 5 _build_buffer(builder, bw_bias.tobytes()), # 6 ] rnn_inputs = [*list(range(9)), -1, -1, -1] if with_aux_input: tensors.extend( [ _t(len(buffers), input_shape), _t(len(buffers) + 1, [num_units, input_size]), _t(len(buffers) + 2, [num_units, input_size]), ] ) rnn_inputs[9:12] = [len(tensors) - 3, len(tensors) - 2, len(tensors) - 1] buffers.extend( [ _build_buffer(builder, np.zeros(input_shape, dtype=np.float32).tobytes()), _build_buffer( builder, np.zeros((num_units, input_size), dtype=np.float32).tobytes() ), _build_buffer( builder, np.zeros((num_units, input_size), dtype=np.float32).tobytes() ), ] ) if merge_outputs: tensors.append(_t(0, output_shape)) outputs = [len(tensors) - 1] else: tensors.extend([_t(0, output_shape), _t(0, output_shape)]) outputs = [len(tensors) - 2, len(tensors) - 1] rnn_op = _build_operator( builder, 0, rnn_inputs, outputs, builtin_options_type=_tfl_builtin_options.BidirectionalSequenceRNNOptions, builtin_options=rnn_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[rnn_op], inputs=[0, 4, 8], outputs=outputs, ) return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[rnn_op_code], buffers=buffers, ) def test_bidirectional_sequence_rnn_none_activation(): """BIDIRECTIONAL_SEQUENCE_RNN with NONE activation lowers the expected equations.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 1, 2, 2 fw_w = np.array([[1.0, 0.0], [0.5, -1.0]], dtype=np.float32) fw_r = np.array([[0.25, 0.0], [0.0, 0.5]], dtype=np.float32) fw_b = np.zeros(num_units, dtype=np.float32) bw_w = np.array([[0.0, 1.0], [-0.5, 0.75]], dtype=np.float32) bw_r = np.array([[0.1, 0.0], [0.0, 0.2]], dtype=np.float32) bw_b = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_bidirectional_sequence_rnn_model( batch, time, input_size, num_units, fw_w, fw_r, fw_b, bw_w, bw_r, bw_b, ActivationFunctionType.NONE, ) ) @I.ir_module class Expected: @R.function def main( x: R.Tensor((2, 1, 2), dtype="float32"), fw_h: R.Tensor((2, 2), dtype="float32"), bw_h: R.Tensor((2, 2), dtype="float32"), ) -> R.Tensor((2, 1, 4), dtype="float32"): R.func_attr({"num_input": 3}) with R.dataflow(): x_t: R.Tensor((2, 2), dtype="float32") = R.squeeze(x, axis=[1]) fw_w_t: R.Tensor((2, 2), dtype="float32") = R.permute_dims(R.const(fw_w), axes=None) fw_x: R.Tensor((2, 2), dtype="float32") = R.matmul(x_t, fw_w_t) fw_r_t: R.Tensor((2, 2), dtype="float32") = R.permute_dims(R.const(fw_r), axes=None) fw_h_proj: R.Tensor((2, 2), dtype="float32") = R.matmul(fw_h, fw_r_t) fw_out: R.Tensor((2, 2), dtype="float32") = R.add( R.add(fw_x, fw_h_proj), R.const(fw_b) ) fw_stacked: R.Tensor((2, 1, 2), dtype="float32") = R.stack((fw_out,), axis=1) bw_w_t: R.Tensor((2, 2), dtype="float32") = R.permute_dims(R.const(bw_w), axes=None) bw_x: R.Tensor((2, 2), dtype="float32") = R.matmul(x_t, bw_w_t) bw_r_t: R.Tensor((2, 2), dtype="float32") = R.permute_dims(R.const(bw_r), axes=None) bw_h_proj: R.Tensor((2, 2), dtype="float32") = R.matmul(bw_h, bw_r_t) bw_out: R.Tensor((2, 2), dtype="float32") = R.add( R.add(bw_x, bw_h_proj), R.const(bw_b) ) bw_stacked: R.Tensor((2, 1, 2), dtype="float32") = R.stack((bw_out,), axis=1) gv: R.Tensor((2, 1, 4), dtype="float32") = R.concat( (fw_stacked, bw_stacked), axis=-1 ) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_bidirectional_sequence_rnn_time_major(): """BIDIRECTIONAL_SEQUENCE_RNN preserves time-major output layout.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 3, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_bidirectional_sequence_rnn_model( batch, time, input_size, num_units, weights, recurrent, bias, weights, recurrent, bias, ActivationFunctionType.NONE, time_major=True, ) ) fn = mod["main"] tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((time, batch, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((time, batch, num_units * 2), "float32"), ) def test_bidirectional_sequence_rnn_rejects_aux_input(): """BIDIRECTIONAL_SEQUENCE_RNN rejects unsupported auxiliary input tensors.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 2, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) with pytest.raises(tvm.error.OpNotImplemented, match="aux input"): _load_model_from_buffer( _build_bidirectional_sequence_rnn_model( batch, time, input_size, num_units, weights, recurrent, bias, weights, recurrent, bias, ActivationFunctionType.NONE, with_aux_input=True, ) ) # ── BIDIRECTIONAL_SEQUENCE_LSTM ────────────────────────────────────────────── def _build_bidirectional_sequence_lstm_model( batch, time, input_size, num_units, fw_w_f, fw_w_c, fw_w_o, fw_r_f, fw_r_c, fw_r_o, fw_b_f, fw_b_c, fw_b_o, bw_w_f, bw_w_c, bw_w_o, bw_r_f, bw_r_c, bw_r_o, bw_b_f, bw_b_c, bw_b_o, activation, *, time_major=False, merge_outputs=True, cell_clip=0.0, proj_clip=0.0, with_aux_input=False, ): """Build a TFLite flatbuffer model with one BIDIRECTIONAL_SEQUENCE_LSTM op. 48 operator inputs. Forward LSTM: indices 0-17, Backward LSTM: indices 18-34, States: indices 35-38. """ builder = flatbuffers.Builder(8192) _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsStart(builder) _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsAddFusedActivationFunction( builder, activation ) _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsAddTimeMajor( builder, time_major ) _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsAddMergeOutputs( builder, merge_outputs ) _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsAddCellClip( builder, cell_clip ) _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsAddProjClip( builder, proj_clip ) lstm_opts = _tfl_bidirectional_sequence_lstm_options.BidirectionalSequenceLSTMOptionsEnd( builder ) lstm_op_code = _build_operator_code(builder, _tfl_builtin_operator.BIDIRECTIONAL_SEQUENCE_LSTM) def _t(buf_idx, shape, is_variable=False): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, is_variable) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) input_shape = [time, batch, input_size] if time_major else [batch, time, input_size] output_size = num_units * 2 if merge_outputs else num_units output_shape = ([time, batch] if time_major else [batch, time]) + [output_size] tensors = [ _t(0, input_shape), # 0: input _t(1, [num_units, input_size]), # 1: fw_w_f _t(2, [num_units, input_size]), # 2: fw_w_c _t(3, [num_units, input_size]), # 3: fw_w_o _t(4, [num_units, num_units]), # 4: fw_r_f _t(5, [num_units, num_units]), # 5: fw_r_c _t(6, [num_units, num_units]), # 6: fw_r_o _t(7, [num_units]), # 7: fw_b_f _t(8, [num_units]), # 8: fw_b_c _t(9, [num_units]), # 9: fw_b_o _t(10, [num_units, input_size]), # 10: bw_w_f _t(11, [num_units, input_size]), # 11: bw_w_c _t(12, [num_units, input_size]), # 12: bw_w_o _t(13, [num_units, num_units]), # 13: bw_r_f _t(14, [num_units, num_units]), # 14: bw_r_c _t(15, [num_units, num_units]), # 15: bw_r_o _t(16, [num_units]), # 16: bw_b_f _t(17, [num_units]), # 17: bw_b_c _t(18, [num_units]), # 18: bw_b_o _t(0, [batch, num_units]), # 19: fw_activation_state (model input) _t(0, [batch, num_units]), # 20: fw_cell_state (model input) _t(0, [batch, num_units]), # 21: bw_activation_state (model input) _t(0, [batch, num_units]), # 22: bw_cell_state (model input) _t(0, output_shape), # 23: output ] # Build operator inputs: 48 total, with unsupported optional inputs set to -1. fw_inputs = [0, -1, 1, 2, 3, -1, 4, 5, 6, -1, -1, -1, -1, 7, 8, 9, -1, -1] bw_inputs = [-1, 10, 11, 12, -1, 13, 14, 15, -1, -1, -1, -1, 16, 17, 18, -1, -1] states = [19, 20, 21, 22] aux_inputs = [-1] * 9 if with_aux_input: tensors.append(_t(0, input_shape)) aux_inputs[0] = len(tensors) - 1 lstm_inputs = fw_inputs + bw_inputs + states + aux_inputs lstm_op = _build_operator( builder, 0, lstm_inputs, [23], builtin_options_type=_tfl_builtin_options.BidirectionalSequenceLSTMOptions, builtin_options=lstm_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[lstm_op], inputs=[0, 19, 20, 21, 22], outputs=[23], ) buffers = [ _build_buffer(builder), # 0: empty _build_buffer(builder, fw_w_f.tobytes()), # 1 _build_buffer(builder, fw_w_c.tobytes()), # 2 _build_buffer(builder, fw_w_o.tobytes()), # 3 _build_buffer(builder, fw_r_f.tobytes()), # 4 _build_buffer(builder, fw_r_c.tobytes()), # 5 _build_buffer(builder, fw_r_o.tobytes()), # 6 _build_buffer(builder, fw_b_f.tobytes()), # 7 _build_buffer(builder, fw_b_c.tobytes()), # 8 _build_buffer(builder, fw_b_o.tobytes()), # 9 _build_buffer(builder, bw_w_f.tobytes()), # 10 _build_buffer(builder, bw_w_c.tobytes()), # 11 _build_buffer(builder, bw_w_o.tobytes()), # 12 _build_buffer(builder, bw_r_f.tobytes()), # 13 _build_buffer(builder, bw_r_c.tobytes()), # 14 _build_buffer(builder, bw_r_o.tobytes()), # 15 _build_buffer(builder, bw_b_f.tobytes()), # 16 _build_buffer(builder, bw_b_c.tobytes()), # 17 _build_buffer(builder, bw_b_o.tobytes()), # 18 ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[lstm_op_code], buffers=buffers, ) def test_bidirectional_sequence_lstm_none_activation(): """BIDIRECTIONAL_SEQUENCE_LSTM with NONE activation keeps both cell activations linear.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 1, 2, 2 def _eye_or_randn(m, n): if m == n: return np.eye(m, dtype=np.float32) return np.arange(m * n, dtype=np.float32).reshape(m, n) / 10.0 fw_w_f = _eye_or_randn(num_units, input_size) fw_w_c = np.array([[1.0, -0.5], [0.25, 0.75]], dtype=np.float32) fw_w_o = np.array([[0.5, 0.25], [-0.25, 1.0]], dtype=np.float32) fw_r_f = _eye_or_randn(num_units, num_units) fw_r_c = np.array([[0.2, 0.0], [0.0, 0.3]], dtype=np.float32) fw_r_o = np.array([[0.1, 0.0], [0.0, 0.2]], dtype=np.float32) fw_b_f = np.zeros(num_units, dtype=np.float32) fw_b_c = np.zeros(num_units, dtype=np.float32) fw_b_o = np.zeros(num_units, dtype=np.float32) bw_w_f = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.float32) bw_w_c = np.array([[0.5, 0.5], [-0.5, 1.0]], dtype=np.float32) bw_w_o = np.array([[0.25, -0.25], [0.75, 0.5]], dtype=np.float32) bw_r_f = np.array([[0.4, 0.0], [0.0, 0.6]], dtype=np.float32) bw_r_c = np.array([[0.3, 0.0], [0.0, 0.2]], dtype=np.float32) bw_r_o = np.array([[0.2, 0.0], [0.0, 0.1]], dtype=np.float32) bw_b_f = np.zeros(num_units, dtype=np.float32) bw_b_c = np.zeros(num_units, dtype=np.float32) bw_b_o = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_bidirectional_sequence_lstm_model( batch, time, input_size, num_units, fw_w_f, fw_w_c, fw_w_o, fw_r_f, fw_r_c, fw_r_o, fw_b_f, fw_b_c, fw_b_o, bw_w_f, bw_w_c, bw_w_o, bw_r_f, bw_r_c, bw_r_o, bw_b_f, bw_b_c, bw_b_o, ActivationFunctionType.NONE, ) ) fn = mod["main"] tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((batch, time, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((batch, time, num_units * 2), "float32"), ) def test_bidirectional_sequence_lstm_time_major(): """BIDIRECTIONAL_SEQUENCE_LSTM preserves time-major output layout.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 3, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_bidirectional_sequence_lstm_model( batch, time, input_size, num_units, weights, weights, weights, recurrent, recurrent, recurrent, bias, bias, bias, weights, weights, weights, recurrent, recurrent, recurrent, bias, bias, bias, ActivationFunctionType.NONE, time_major=True, ) ) fn = mod["main"] tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((time, batch, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((time, batch, num_units * 2), "float32"), ) def test_bidirectional_sequence_lstm_rejects_aux_input(): """BIDIRECTIONAL_SEQUENCE_LSTM rejects unsupported auxiliary inputs.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 2, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) with pytest.raises(tvm.error.OpNotImplemented, match="aux input"): _load_model_from_buffer( _build_bidirectional_sequence_lstm_model( batch, time, input_size, num_units, weights, weights, weights, recurrent, recurrent, recurrent, bias, bias, bias, weights, weights, weights, recurrent, recurrent, recurrent, bias, bias, bias, ActivationFunctionType.NONE, with_aux_input=True, ) ) # ── UNIDIRECTIONAL_SEQUENCE_RNN ─────────────────────────────────────────────── def _build_unidirectional_sequence_rnn_model( batch, time, input_size, num_units, weights, recurrent_weights, bias, activation, *, time_major=False, ): """Build a minimal TFLite flatbuffer model containing one UNIDIRECTIONAL_SEQUENCE_RNN op. Tensor layout (indices 0-5): 0 - input [batch, time, input_size] (or [time, batch, input_size] if time_major) 1 - input_weights [num_units, input_size] (constant) 2 - recurrent_wts [num_units, num_units] (constant) 3 - bias [num_units] (constant) 4 - hidden_state [batch, num_units] (variable, zero-initialised) 5 - output [batch, time, num_units] """ builder = flatbuffers.Builder(4096) _tfl_sequence_rnn_options.SequenceRNNOptionsStart(builder) _tfl_sequence_rnn_options.SequenceRNNOptionsAddTimeMajor(builder, time_major) _tfl_sequence_rnn_options.SequenceRNNOptionsAddFusedActivationFunction(builder, activation) rnn_opts = _tfl_sequence_rnn_options.SequenceRNNOptionsEnd(builder) rnn_op_code = _build_operator_code(builder, _tfl_builtin_operator.UNIDIRECTIONAL_SEQUENCE_RNN) input_shape = [time, batch, input_size] if time_major else [batch, time, input_size] def _t(buf_idx, shape, is_variable=False): shape_vec = _tflite_shape(builder, shape) _tfl_tensor.TensorStart(builder) _tfl_tensor.TensorAddBuffer(builder, buf_idx) _tfl_tensor.TensorAddHasRank(builder, True) _tfl_tensor.TensorAddIsVariable(builder, is_variable) _tfl_tensor.TensorAddShape(builder, shape_vec) _tfl_tensor.TensorAddType(builder, _tfl_tensor_type.FLOAT32) return _tfl_tensor.TensorEnd(builder) tensors = [ _t(0, input_shape), _t(1, [num_units, input_size]), _t(2, [num_units, num_units]), _t(3, [num_units]), _t(4, [batch, num_units], is_variable=True), _t(5, [batch, time, num_units]), ] rnn_op = _build_operator( builder, 0, [0, 1, 2, 3, 4], [5], builtin_options_type=_tfl_builtin_options.SequenceRNNOptions, builtin_options=rnn_opts, ) subgraph = _build_subgraph( builder, tensors=tensors, operators=[rnn_op], inputs=[0], outputs=[5], ) buffers = [ _build_buffer(builder), _build_buffer(builder, weights.tobytes()), _build_buffer(builder, recurrent_weights.tobytes()), _build_buffer(builder, bias.tobytes()), _build_buffer(builder), _build_buffer(builder), ] return _finish_tflite_model( builder, subgraph=subgraph, operator_codes=[rnn_op_code], buffers=buffers, ) def test_unidirectional_sequence_rnn_none_activation(): """UNIDIRECTIONAL_SEQUENCE_RNN with NONE activation, time=1, lowers to matmul/add/stack. Cell equation: h_t = x_t @ W.T + h_{t-1} @ Wr.T + b (no activation for NONE) """ from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 1, 2, 2 weights = np.eye(num_units, input_size, dtype=np.float32) recurrent_weights = np.eye(num_units, dtype=np.float32) bias = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_unidirectional_sequence_rnn_model( batch, time, input_size, num_units, weights, recurrent_weights, bias, ActivationFunctionType.NONE, ) ) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 1, 2), dtype="float32")) -> R.Tensor((2, 1, 2), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 2), dtype="float32") = R.squeeze(x, axis=[1]) lv1: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv2: R.Tensor((2, 2), dtype="float32") = R.matmul(lv, lv1) lv3: R.Tensor((2, 2), dtype="float32") = R.zeros(R.shape([2, 2]), dtype="float32") lv4: R.Tensor((2, 2), dtype="float32") = R.permute_dims( R.const(np.eye(2, dtype=np.float32)), axes=None ) lv5: R.Tensor((2, 2), dtype="float32") = R.matmul(lv3, lv4) lv6: R.Tensor((2, 2), dtype="float32") = R.add(lv2, lv5) lv7: R.Tensor((2, 2), dtype="float32") = R.add( lv6, R.const(np.zeros(2, dtype=np.float32)) ) gv: R.Tensor((2, 1, 2), dtype="float32") = R.stack((lv7,), axis=1) R.output(gv) return gv tvm.ir.assert_structural_equal(mod, Expected) def test_unidirectional_sequence_rnn_relu_activation(): """UNIDIRECTIONAL_SEQUENCE_RNN with RELU activation and multiple time steps.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 2, 3, 4, 8 np.random.seed(42) weights = np.random.randn(num_units, input_size).astype(np.float32) recurrent_weights = np.random.randn(num_units, num_units).astype(np.float32) bias = np.random.randn(num_units).astype(np.float32) mod = _load_model_from_buffer( _build_unidirectional_sequence_rnn_model( batch, time, input_size, num_units, weights, recurrent_weights, bias, ActivationFunctionType.RELU, ) ) fn = mod["main"] assert len(fn.params) == 1, "only the sequence input should be a graph input" tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((batch, time, input_size), "float32"), ) tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((batch, time, num_units), "float32"), ) def test_unidirectional_sequence_rnn_time_major(): """UNIDIRECTIONAL_SEQUENCE_RNN with time_major=True transposes before unrolling.""" from tflite.ActivationFunctionType import ActivationFunctionType batch, time, input_size, num_units = 3, 4, 2, 5 np.random.seed(7) weights = np.random.randn(num_units, input_size).astype(np.float32) recurrent_weights = np.random.randn(num_units, num_units).astype(np.float32) bias = np.zeros(num_units, dtype=np.float32) mod = _load_model_from_buffer( _build_unidirectional_sequence_rnn_model( batch, time, input_size, num_units, weights, recurrent_weights, bias, ActivationFunctionType.NONE, time_major=True, ) ) fn = mod["main"] # Input to the graph is the raw time-major tensor [time, batch, input_size]. tvm.ir.assert_structural_equal( fn.params[0].ty, relax.TensorType((time, batch, input_size), "float32"), ) # Output is always batch-major [batch, time, num_units]. tvm.ir.assert_structural_equal( fn.ret_ty, relax.TensorType((batch, time, num_units), "float32"), ) def test_real(): class Real(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 4), dtype=tf.complex64)]) def func(self, x): return tf.math.real(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 4, 2), dtype="float32")) -> R.Tensor((2, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 4, 1), dtype="float32") = R.strided_slice( x, (R.prim_value(-1),), (R.prim_value(0),), (R.prim_value(1),), (R.prim_value(1),), assume_inbound=False, ) gv: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv, axis=[-1]) R.output(gv) return gv verify(Real, Expected) def test_imag(): class Imag(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 4), dtype=tf.complex64)]) def func(self, x): return tf.math.imag(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 4, 2), dtype="float32")) -> R.Tensor((2, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 4, 1), dtype="float32") = R.strided_slice( x, (R.prim_value(-1),), (R.prim_value(1),), (R.prim_value(2),), (R.prim_value(1),), assume_inbound=False, ) gv: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv, axis=[-1]) R.output(gv) return gv verify(Imag, Expected) def test_complex_abs(): class ComplexAbs(tf.Module): @tf.function(input_signature=[tf.TensorSpec(shape=(2, 4), dtype=tf.complex64)]) def func(self, x): return tf.math.abs(x) @I.ir_module class Expected: @R.function def main(x: R.Tensor((2, 4, 2), dtype="float32")) -> R.Tensor((2, 4), dtype="float32"): R.func_attr({"num_input": 1}) with R.dataflow(): lv: R.Tensor((2, 4, 1), dtype="float32") = R.strided_slice( x, (R.prim_value(-1),), (R.prim_value(0),), (R.prim_value(1),), (R.prim_value(1),), assume_inbound=False, ) lv1: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv, axis=[-1]) lv2: R.Tensor((2, 4, 1), dtype="float32") = R.strided_slice( x, (R.prim_value(-1),), (R.prim_value(1),), (R.prim_value(2),), (R.prim_value(1),), assume_inbound=False, ) lv3: R.Tensor((2, 4), dtype="float32") = R.squeeze(lv2, axis=[-1]) lv4: R.Tensor((2, 4), dtype="float32") = R.multiply(lv1, lv1) lv5: R.Tensor((2, 4), dtype="float32") = R.multiply(lv3, lv3) lv6: R.Tensor((2, 4), dtype="float32") = R.add(lv4, lv5) gv: R.Tensor((2, 4), dtype="float32") = R.sqrt(lv6) R.output(gv) return gv verify(ComplexAbs, Expected) if __name__ == "__main__": pytest.main(["-s", __file__])