This is an automated email from the ASF dual-hosted git repository.
tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new f792a1d1aa [Relax][Frontend][TFLite] Support StableHLO shape ops
(#20114)
f792a1d1aa is described below
commit f792a1d1aa631ee8498600d0398d219df40d816c
Author: Hongyi Wu <[email protected]>
AuthorDate: Tue Aug 18 12:52:53 2026 +0800
[Relax][Frontend][TFLite] Support StableHLO shape ops (#20114)
## Summary
This PR adds Relax TFLite frontend support for the remaining StableHLO
shape
operators tracked by #19519:
- `STABLEHLO_RESHAPE` -> `R.reshape`
- `STABLEHLO_SLICE` -> `R.strided_slice`
- `STABLEHLO_TRANSPOSE` -> `R.permute_dims`
It carries forward the implementation from #19869 by @Mohxen onto the
current
`main` branch and addresses the outstanding review feedback by using
explicit
`ValueError` checks for the input and output arity of all three new
converters.
## Design
### StableHLO reshape
`STABLEHLO_RESHAPE` has one tensor input and a statically described
result
shape. The converter reads that shape from the TFLite output tensor
metadata
and emits `relax.op.reshape`.
### StableHLO slice
`STABLEHLO_SLICE` stores `start_indices`, `limit_indices`, and `strides`
in
`StablehloSliceOptions`. The converter parses those vectors, applies
them to all
input axes, and emits `relax.op.strided_slice`.
### StableHLO transpose
`STABLEHLO_TRANSPOSE` stores its permutation in
`StablehloTransposeOptions`. The converter parses the permutation and
emits
`relax.op.permute_dims`.
## Operator Support
| Operator | TFLite metadata | Relax lowering | Supported subset |
|---|---|---|---|
| `STABLEHLO_RESHAPE` | output tensor shape | `R.reshape` | static
result shape |
| `STABLEHLO_SLICE` | start, limit, and stride vectors |
`R.strided_slice` | static slice attributes |
| `STABLEHLO_TRANSPOSE` | permutation vector | `R.permute_dims` | static
permutation |
## Tests
The tests manually build minimal TFLite flatbuffers for each StableHLO
operator and compare the imported Relax IR with
`tvm.ir.assert_structural_equal`. The slice fixture exercises non-unit
strides,
and the transpose fixture uses a nontrivial three-dimensional
permutation.
Local validation:
```bash
python -m ruff format --check \
python/tvm/relax/frontend/tflite/tflite_frontend.py \
tests/python/relax/test_frontend_tflite.py
python -m ruff check \
python/tvm/relax/frontend/tflite/tflite_frontend.py \
tests/python/relax/test_frontend_tflite.py
python -m py_compile \
python/tvm/relax/frontend/tflite/tflite_frontend.py \
tests/python/relax/test_frontend_tflite.py
python -m pytest tests/python/relax/test_frontend_tflite.py \
-k "stablehlo_reshape or stablehlo_slice or stablehlo_transpose" -q
```
Result:
```text
ruff format --check: 2 files already formatted
ruff check: All checks passed
py_compile: passed
targeted StableHLO shape tests: 3 passed, 551 deselected
```
## References
- Completes the remaining StableHLO shape-operator items in #19519.
- Continues and supersedes #19869 by @Mohxen.
---------
Co-authored-by: Mohxen <[email protected]>
---
.../tvm/relax/frontend/tflite/tflite_frontend.py | 70 ++++++
tests/python/relax/test_frontend_tflite.py | 257 +++++++++++++++++++++
2 files changed, 327 insertions(+)
diff --git a/python/tvm/relax/frontend/tflite/tflite_frontend.py
b/python/tvm/relax/frontend/tflite/tflite_frontend.py
index d271221fc7..13d78c4c30 100644
--- a/python/tvm/relax/frontend/tflite/tflite_frontend.py
+++ b/python/tvm/relax/frontend/tflite/tflite_frontend.py
@@ -391,6 +391,7 @@ class OperatorConverter:
"STABLEHLO_REDUCE": self._convert_stablehlo_reduce,
"STABLEHLO_REDUCE_WINDOW": self._convert_stablehlo_reduce_window,
"STABLEHLO_REMAINDER": self._convert_stablehlo_remainder,
+ "STABLEHLO_RESHAPE": self._convert_stablehlo_reshape,
"STABLEHLO_RNG_BIT_GENERATOR":
self._convert_stablehlo_rng_bit_generator,
"STABLEHLO_RSQRT":
functools.partial(self._convert_stablehlo_unary, relax_op=_op.rsqrt),
"STABLEHLO_SCATTER": self._convert_stablehlo_scatter,
@@ -400,11 +401,13 @@ class OperatorConverter:
"STABLEHLO_SHIFT_LEFT": functools.partial(
self._convert_stablehlo_binary, relax_op=_op.left_shift
),
+ "STABLEHLO_SLICE": self._convert_stablehlo_slice,
"STABLEHLO_SORT": self._convert_stablehlo_sort,
"STABLEHLO_SUBTRACT": functools.partial(
self._convert_stablehlo_binary, relax_op=_op.subtract
),
"STABLEHLO_TANH": functools.partial(self._convert_stablehlo_unary,
relax_op=_op.tanh),
+ "STABLEHLO_TRANSPOSE": self._convert_stablehlo_transpose,
"STABLEHLO_WHILE": self._convert_stablehlo_while,
"SQUEEZE": self.convert_squeeze,
"STRIDED_SLICE": self.convert_strided_slice,
@@ -3188,6 +3191,73 @@ class OperatorConverter:
reshaped = self.bb.normalize(relax.op.reshape(in_expr,
intermediate_shape))
return self.bb.normalize(relax.op.broadcast_to(reshaped, output_shape))
+ def _convert_stablehlo_reshape(self, op):
+ """Convert STABLEHLO_RESHAPE to Relax."""
+ input_tensors = self.get_input_tensors(op)
+ if len(input_tensors) != 1:
+ raise ValueError(
+ f"STABLEHLO_RESHAPE expects exactly 1 input tensor, but got
{len(input_tensors)}"
+ )
+ output_tensors = self.get_output_tensors(op)
+ if len(output_tensors) != 1:
+ raise ValueError(
+ f"STABLEHLO_RESHAPE expects exactly 1 output tensor, but got
{len(output_tensors)}"
+ )
+
+ in_expr = self.get_tensor_expr(input_tensors[0])
+ output_shape = self._get_relax_tensor_shape(output_tensors[0])
+ return self.bb.normalize(relax.op.reshape(in_expr, output_shape))
+
+ def _convert_stablehlo_slice(self, op):
+ """Convert STABLEHLO_SLICE to Relax."""
+ from tflite.StablehloSliceOptions import StablehloSliceOptions
+
+ input_tensors = self.get_input_tensors(op)
+ if len(input_tensors) != 1:
+ raise ValueError(
+ f"STABLEHLO_SLICE expects exactly 1 input tensor, but got
{len(input_tensors)}"
+ )
+ output_tensors = self.get_output_tensors(op)
+ if len(output_tensors) != 1:
+ raise ValueError(
+ f"STABLEHLO_SLICE expects exactly 1 output tensor, but got
{len(output_tensors)}"
+ )
+
+ opts = self._get_stablehlo_options(op, StablehloSliceOptions)
+ begin = [int(d) for d in opts.StartIndicesAsNumpy()]
+ end = [int(d) for d in opts.LimitIndicesAsNumpy()]
+ strides = [int(d) for d in opts.StridesAsNumpy()]
+ axes = list(range(len(begin)))
+
+ in_expr = self.get_tensor_expr(input_tensors[0])
+ return self.bb.normalize(
+ relax.op.strided_slice(in_expr, axes=axes, begin=begin, end=end,
strides=strides)
+ )
+
+ def _convert_stablehlo_transpose(self, op):
+ """Convert STABLEHLO_TRANSPOSE to Relax."""
+ from tflite.StablehloTransposeOptions import StablehloTransposeOptions
+
+ input_tensors = self.get_input_tensors(op)
+ if len(input_tensors) != 1:
+ raise ValueError(
+ f"STABLEHLO_TRANSPOSE expects exactly 1 input tensor, but got
{len(input_tensors)}"
+ )
+ output_tensors = self.get_output_tensors(op)
+ if len(output_tensors) != 1:
+ raise ValueError(
+ "STABLEHLO_TRANSPOSE expects exactly 1 output tensor, "
+ f"but got {len(output_tensors)}"
+ )
+
+ opts = self._get_stablehlo_options(op, StablehloTransposeOptions)
+ permutation = [int(d) for d in opts.PermutationAsNumpy()]
+ if self._is_tflite_complex64_type(input_tensors[0].tensor.Type()):
+ permutation.append(len(permutation))
+
+ in_expr = self.get_tensor_expr(input_tensors[0])
+ return self.bb.normalize(relax.op.permute_dims(in_expr,
axes=permutation))
+
def _convert_stablehlo_iota(self, op):
"""Convert STABLEHLO_IOTA to Relax (arange + broadcast)."""
from tflite.StablehloIotaOptions import StablehloIotaOptions
diff --git a/tests/python/relax/test_frontend_tflite.py
b/tests/python/relax/test_frontend_tflite.py
index 1561826b17..e7ebbeaf92 100644
--- a/tests/python/relax/test_frontend_tflite.py
+++ b/tests/python/relax/test_frontend_tflite.py
@@ -4662,7 +4662,9 @@ _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_slice_opts = _get_tflite_schema_module("StablehloSliceOptions")
_tfl_stablehlo_sort_opts = _get_tflite_schema_module("StablehloSortOptions")
+_tfl_stablehlo_transpose_opts =
_get_tflite_schema_module("StablehloTransposeOptions")
_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")
@@ -9512,6 +9514,261 @@ def test_stablehlo_concatenate(dimension):
tvm.ir.assert_structural_equal(mod, Expected)
+def _build_stablehlo_reshape_model(input_shape, output_shape,
tensor_type=_tfl_tensor_type.FLOAT32):
+ """STABLEHLO_RESHAPE with given input and output shapes."""
+ builder = flatbuffers.Builder(1024)
+
+ builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_RESHAPE")
+ op_code = _build_operator_code(builder, builtin_op)
+
+ tensors = [
+ _build_tensor(builder, 0, input_shape, tensor_type=tensor_type),
+ _build_tensor(builder, 1, output_shape, tensor_type=tensor_type),
+ ]
+ 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_reshape():
+ """TFLite StableHLO RESHAPE lowers to Relax reshape."""
+ mod = _load_model_from_buffer(
+ _build_stablehlo_reshape_model(input_shape=[2, 3], output_shape=[3, 2])
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((3, 2),
dtype="float32"):
+ R.func_attr({"num_input": 1})
+ with R.dataflow():
+ gv: R.Tensor((3, 2), dtype="float32") = R.reshape(x, (3, 2))
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def test_stablehlo_reshape_scalar():
+ """TFLite StableHLO RESHAPE supports a rank-0 output."""
+ mod =
_load_model_from_buffer(_build_stablehlo_reshape_model(input_shape=[1],
output_shape=[]))
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(x: R.Tensor((1,), dtype="float32")) -> R.Tensor((),
dtype="float32"):
+ R.func_attr({"num_input": 1})
+ with R.dataflow():
+ gv: R.Tensor((), dtype="float32") = R.reshape(x, ())
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def test_stablehlo_reshape_complex64():
+ """TFLite StableHLO RESHAPE preserves the Relax complex pair axis."""
+ mod = _load_model_from_buffer(
+ _build_stablehlo_reshape_model(
+ input_shape=[2, 3],
+ output_shape=[3, 2],
+ tensor_type=_tfl_tensor_type.COMPLEX64,
+ )
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(x: R.Tensor((2, 3, 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.reshape(x, (3, 2,
2))
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def _build_stablehlo_slice_model(input_shape, start_indices, limit_indices,
strides, output_shape):
+ """STABLEHLO_SLICE with static start, limit, and stride attributes."""
+ builder = flatbuffers.Builder(1024)
+
+ start_vec = _tflite_int64_vector(
+ builder,
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsStartStartIndicesVector,
+ start_indices,
+ )
+ limit_vec = _tflite_int64_vector(
+ builder,
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsStartLimitIndicesVector,
+ limit_indices,
+ )
+ strides_vec = _tflite_int64_vector(
+ builder,
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsStartStridesVector,
+ strides,
+ )
+
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsStart(builder)
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsAddStartIndices(builder,
start_vec)
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsAddLimitIndices(builder,
limit_vec)
+ _tfl_stablehlo_slice_opts.StablehloSliceOptionsAddStrides(builder,
strides_vec)
+ slice_opts = _tfl_stablehlo_slice_opts.StablehloSliceOptionsEnd(builder)
+
+ builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_SLICE")
+ op_code = _build_operator_code(builder, builtin_op)
+
+ tensors = [
+ _build_tensor(builder, 0, input_shape),
+ _build_tensor(builder, 1, output_shape),
+ ]
+ op = _build_operator(
+ builder,
+ 0,
+ [0],
+ [1],
+ builtin_options2_type=_tfl_builtin_options2.StablehloSliceOptions,
+ builtin_options2=slice_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_slice():
+ """TFLite StableHLO SLICE lowers to Relax strided_slice."""
+ mod = _load_model_from_buffer(
+ _build_stablehlo_slice_model(
+ input_shape=[4, 5],
+ start_indices=[1, 0],
+ limit_indices=[4, 4],
+ strides=[2, 2],
+ output_shape=[2, 2],
+ )
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(x: R.Tensor((4, 5), 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, 0], end=[4, 4], strides=[2, 2]
+ )
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def _build_stablehlo_transpose_model(
+ input_shape, permutation, output_shape,
tensor_type=_tfl_tensor_type.FLOAT32
+):
+ """STABLEHLO_TRANSPOSE with a static permutation."""
+ builder = flatbuffers.Builder(1024)
+
+ perm_vec = _tflite_int64_vector(
+ builder,
+
_tfl_stablehlo_transpose_opts.StablehloTransposeOptionsStartPermutationVector,
+ permutation,
+ )
+ _tfl_stablehlo_transpose_opts.StablehloTransposeOptionsStart(builder)
+
_tfl_stablehlo_transpose_opts.StablehloTransposeOptionsAddPermutation(builder,
perm_vec)
+ transpose_opts =
_tfl_stablehlo_transpose_opts.StablehloTransposeOptionsEnd(builder)
+
+ builtin_op = _get_stablehlo_builtin_operator("STABLEHLO_TRANSPOSE")
+ op_code = _build_operator_code(builder, builtin_op)
+
+ tensors = [
+ _build_tensor(builder, 0, input_shape, tensor_type=tensor_type),
+ _build_tensor(builder, 1, output_shape, tensor_type=tensor_type),
+ ]
+ op = _build_operator(
+ builder,
+ 0,
+ [0],
+ [1],
+ builtin_options2_type=_tfl_builtin_options2.StablehloTransposeOptions,
+ builtin_options2=transpose_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_transpose():
+ """TFLite StableHLO TRANSPOSE lowers to Relax permute_dims."""
+ mod = _load_model_from_buffer(
+ _build_stablehlo_transpose_model(
+ input_shape=[2, 3, 4], permutation=[1, 2, 0], output_shape=[3, 4,
2]
+ )
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(x: R.Tensor((2, 3, 4), dtype="float32")) -> R.Tensor((3, 4,
2), dtype="float32"):
+ R.func_attr({"num_input": 1})
+ with R.dataflow():
+ gv: R.Tensor((3, 4, 2), dtype="float32") = R.permute_dims(x,
axes=[1, 2, 0])
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def test_stablehlo_transpose_complex64():
+ """TFLite StableHLO TRANSPOSE leaves the Relax complex pair axis
trailing."""
+ mod = _load_model_from_buffer(
+ _build_stablehlo_transpose_model(
+ input_shape=[2, 3, 4],
+ permutation=[1, 2, 0],
+ output_shape=[3, 4, 2],
+ tensor_type=_tfl_tensor_type.COMPLEX64,
+ )
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(x: R.Tensor((2, 3, 4, 2), dtype="float32")) -> R.Tensor(
+ (3, 4, 2, 2), dtype="float32"
+ ):
+ R.func_attr({"num_input": 1})
+ with R.dataflow():
+ gv: R.Tensor((3, 4, 2, 2), dtype="float32") =
R.permute_dims(x, axes=[1, 2, 0, 3])
+ 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)