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

15178 lines
526 KiB
Python

# 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("<iiii", 1, 1, 1, 1)
t_oshape = _build_tensor(builder, 4, [4], tensor_type=_tfl_tensor_type.INT32)
_tfl_transpose_conv_options.TransposeConvOptionsStart(builder)
_tfl_transpose_conv_options.TransposeConvOptionsAddStrideH(builder, 1)
_tfl_transpose_conv_options.TransposeConvOptionsAddStrideW(builder, 1)
_tfl_transpose_conv_options.TransposeConvOptionsAddPadding(builder, 1) # VALID
_tfl_transpose_conv_options.TransposeConvOptionsAddFusedActivationFunction(builder, 0)
tc_opts = _tfl_transpose_conv_options.TransposeConvOptionsEnd(builder)
tc_op = _build_operator(
builder,
0,
[4, 1, 0, 2],
[3],
builtin_options_type=_tfl_builtin_options.TransposeConvOptions,
builtin_options=tc_opts,
)
subgraph = _build_subgraph(
builder,
tensors=[t_in, t_wt, t_bi, t_ou, t_oshape],
operators=[tc_op],
inputs=[0, 1, 2],
outputs=[3],
)
operator_codes = [_build_operator_code(builder, _tfl_builtin_operator.TRANSPOSE_CONV)]
buf = _finish_tflite_model(
builder,
subgraph=subgraph,
operator_codes=operator_codes,
buffers=[
_build_buffer(builder),
_build_buffer(builder),
_build_buffer(builder),
_build_buffer(builder),
_build_buffer(builder, oshape_data),
],
)
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, 1, 1, 1), dtype="int8"),
tvmgen_tensor_1: R.Tensor((1, 1, 1, 1), dtype="int8"),
tvmgen_tensor_2: R.Tensor((1,), dtype="int32"),
) -> 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__])