15178 lines
526 KiB
Python
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__])
|