Skip to content

[Relax][Frontend][TFLite] Support StableHLO region-based ops and multi-subgraph models#19587

Merged
tlopex merged 11 commits into
apache:mainfrom
Aharrypotter:stablehlo_tflite_ops_pr2
May 21, 2026
Merged

[Relax][Frontend][TFLite] Support StableHLO region-based ops and multi-subgraph models#19587
tlopex merged 11 commits into
apache:mainfrom
Aharrypotter:stablehlo_tflite_ops_pr2

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds Relax TFLite frontend support for 10 additional StableHLO builtin
operators from #19519 item I, building on the 29 ops merged in PR #19536.

The first 5 ops are direct single-subgraph converters: CBRT, REMAINDER,
DYNAMIC_UPDATE_SLICE, DOT_GENERAL, and CONVOLUTION. The remaining 5 ops
are region/subgraph-based: REDUCE, REDUCE_WINDOW, SORT, SCATTER, and
COMPOSITE. To support these, the TFLite frontend is extended to accept
multi-subgraph models while still converting only Subgraphs(0) into the
Relax main function. Region subgraphs are consumed by their parent op
converters as needed.

Relates to #19519.

Changes

  1. Single-subgraph ops

    • CBRT — sign-preserving composite expression:
      where(x < 0, -power(-x, 1/3), power(x, 1/3)). Float dtype only.
    • REMAINDER — truncating remainder via x - y * trunc(x / y), matching
      StableHLO semantics (sign follows dividend). Float dtype only.
    • DYNAMIC_UPDATE_SLICE — static start indices + static shapes only, lowered
      to R.scatter_nd with a coordinate grid generated via np.indices.
      Runtime starts and out-of-bounds ranges raise OpNotImplemented.
    • DOT_GENERAL — canonical 2D matmul subset: no batching dims,
      lhs_contracting=[1], rhs_contracting=[0], lowered to R.matmul.
    • CONVOLUTION — canonical 2D NHWC/HWIO subset with BatchGroupCount=1,
      FeatureGroupCount=1, lowered to R.nn.conv2d. Non-canonical dimension
      numbers and grouped/depthwise conv raise OpNotImplemented.
  2. Multi-subgraph infrastructure

    • Lift from_tflite() assertion from model.SubgraphsLength() == 1 to
      model.SubgraphsLength() >= 1. Only Subgraphs(0) is converted into the
      Relax main function.
    • Limit _input_type() to Subgraphs(0) inputs, preventing region
      parameters from leaking as Relax main function parameters.
    • Add _get_stablehlo_simple_body_op helper for validating and extracting
      the single operator from a region body subgraph.
    • Extend test helper _finish_tflite_model with extra_subgraphs parameter
      for constructing multi-subgraph TFLite flatbuffers.
  3. Region/subgraph ops

    • REDUCE — single-op reducer body subgraph. Supports ADDR.sum,
      MAXIMUMR.max, MINIMUMR.min, MULTIPLYR.prod.
      Init value must match the reducer identity element.
    • SORT — single-op comparator body subgraph. LT → ascending sort,
      GT → descending sort via R.sort. IsStable is not mapped.
    • REDUCE_WINDOW — NHWC 4D 2D-pooling subset with MAXIMUM reducer and
      identity init, lowered to R.nn.max_pool2d. BaseDilations must be all 1.
    • SCATTER — single-op update computation body subgraph. Supports
      ADD/MAXIMUM/MINIMUM/MULTIPLYR.scatter_nd with the
      corresponding reduction mode. Only canonical point-update semantics
      (no window dims).
    • COMPOSITE — inlines a decomposition subgraph through a recursive
      OperatorConverter with an isolated ExprTable, so decomposition tensor
      bindings cannot overwrite main graph bindings. Only supports composites
      without CompositeAttributes.
  4. Not included

    • STABLEHLO_RESHAPE, STABLEHLO_TRANSPOSE, and STABLEHLO_SLICE
    • WHILE, CUSTOM_CALL, and RNG_BIT_GENERATOR
  5. Bug fix

    • Fixed DYNAMIC_UPDATE_SLICE scatter_nd indices layout: np.indices
      returns (rank, *update_shape) but scatter_nd expects
      (*update_shape, rank). Added np.moveaxis to transpose the coordinate
      axis from first to last position.

Testing

All tests use manually-built minimal TFLite flatbuffers with
tvm.ir.assert_structural_equal. Region/subgraph tests construct the smallest
valid body/comparator/update subgraphs. BuiltinOptions2 ops construct their
options via the FlatBuffers schema API.

python -m pytest tests/python/relax/test_frontend_tflite.py -k stablehlo -q

Result

  • 39 StableHLO operators registered in the Relax TFLite frontend (29 from
    PR [Relax][Frontend][TFLite] Add initial StableHLO builtin operator support #19536 + 10 from this PR).

  • 77 StableHLO test cases covering all registered ops, including
    structural-equal tests and unsupported/error-path checks:

    • REMAINDER truncating semantics
    • DYNAMIC_UPDATE_SLICE with dynamic starts and out-of-bounds starts
    • DOT_GENERAL with non-canonical contracting dimensions
    • CONVOLUTION with non-canonical dimension numbers and FeatureGroupCount > 1
    • REDUCE with unsupported reducer and non-identity init value
    • SORT with unsupported comparator and stable sort
    • REDUCE_WINDOW with unsupported reducer and base dilation
    • SCATTER with unsupported reducer and update window dims
    • COMPOSITE with composite attributes and scope isolation
    • Multi-subgraph model with unused subgraphs
  • All 77 StableHLO tests pass.

References

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly expands the TFLite frontend's support for StableHLO operators in TVM Relax. It introduces conversions for several complex operations, including REDUCE, SCATTER, SORT, CONVOLUTION, and DOT_GENERAL. Additionally, it adds support for handling multiple subgraphs, which are used as region bodies for these operators. Feedback suggests simplifying the STABLEHLO_CBRT implementation using relax.op.sign and relax.op.abs for better conciseness and potential backend efficiency. Another suggestion points out an opportunity to optimize constant handling during the decomposition of STABLEHLO_COMPOSITE operators by reusing existing expressions instead of re-creating them.

Comment thread python/tvm/relax/frontend/tflite/tflite_frontend.py
Comment thread python/tvm/relax/frontend/tflite/tflite_frontend.py Outdated
@Aharrypotter

Copy link
Copy Markdown
Contributor Author

cc @tlopex

@tlopex tlopex merged commit fff3b4b into apache:main May 21, 2026
9 checks passed
Aharrypotter added a commit to Aharrypotter/tvm that referenced this pull request May 26, 2026
Add Relax TFLite frontend support for the TFLite control-flow and multi-subgraph operators from apache#19519 item F: CALL, IF, WHILE, and the no-op subset of CALL_ONCE.

The implementation builds on the multi-subgraph import infrastructure from apache#19587. Referenced subgraphs are lowered to private Relax functions with isolated expression tables and shared lowering state, while the main TFLite subgraph remains the Relax main function.

- Lower CALL targets to private Relax functions and emit ordinary Relax function calls.

- Lower IF branches to private Relax functions and emit a private wrapper function containing Relax If.

- Lower WHILE cond/body subgraphs to private Relax functions and emit a recursive private Relax loop function.

- Support CALL_ONCE only when the init subgraph is empty; non-empty init subgraphs remain guarded because they may model resource initialization side effects.

- Validate referenced subgraph input/output counts and static tensor metadata at CALL, IF, and WHILE boundaries.

Added hand-built TFLite flatbuffer tests with structural equality coverage for CALL, IF, WHILE, empty CALL_ONCE, multi-output CALL, multi-output IF, and two-loop-var WHILE.

Added unsupported-path tests for non-empty CALL_ONCE, invalid subgraph indices, arity mismatches, non-bool IF/WHILE conditions, and static metadata mismatches.
Aharrypotter added a commit to Aharrypotter/tvm that referenced this pull request May 26, 2026
Add Relax TFLite frontend support for the TFLite control-flow and multi-subgraph operators from apache#19519 item F: CALL, IF, WHILE, and the no-op subset of CALL_ONCE.

The implementation builds on the multi-subgraph import infrastructure from apache#19587. Referenced subgraphs are lowered to private Relax functions with isolated expression tables and shared lowering state, while the main TFLite subgraph remains the Relax main function.

- Lower CALL targets to private Relax functions and emit ordinary Relax function calls.

- Lower IF branches to private Relax functions and emit a private wrapper function containing Relax If.

- Lower WHILE cond/body subgraphs to private Relax functions and emit a recursive private Relax loop function.

- Support CALL_ONCE only when the init subgraph is empty; non-empty init subgraphs remain guarded because they may model resource initialization side effects.

- Validate referenced subgraph input/output counts and static tensor metadata at CALL, IF, and WHILE boundaries.

Added hand-built TFLite flatbuffer tests with structural equality coverage for CALL, IF, WHILE, empty CALL_ONCE, multi-output CALL, multi-output IF, and two-loop-var WHILE.

Added unsupported-path tests for non-empty CALL_ONCE, invalid subgraph indices, arity mismatches, non-bool IF/WHILE conditions, and static metadata mismatches.
tlopex pushed a commit that referenced this pull request May 27, 2026
…rs (#19616)

## Summary

This PR adds Relax TFLite frontend support for the TFLite builtin
control-flow / multi-subgraph operator family from #19519 item F:
`CALL`, `IF`, `WHILE`, and `CALL_ONCE`.

It builds on the multi-subgraph import infrastructure merged in PR
#19587.
The frontend already accepts TFLite models with extra subgraphs while
converting
only `Subgraphs(0)` into the Relax `main` function. This PR uses those
extra
subgraphs as callable or control-flow regions for the TFLite
control-flow
operators.

The supported subset is intentionally pure tensor and guard-first:

- `CALL` lowers a referenced TFLite subgraph to a private Relax function
and
  emits a direct call.
- `IF` lowers the then/else subgraphs to private Relax functions and
emits a
  private wrapper function containing Relax `If`.
- `WHILE` lowers the cond/body subgraphs to private Relax functions and
emits a
  recursive private Relax function for the loop.
- `CALL_ONCE` supports the empty-init no-op subset and explicitly
rejects
  non-empty or resource-like init patterns.

This PR does not model resource variable side effects. Those cases
remain
explicitly guarded instead of being imported with incorrect pure
functional
semantics.

## Design

### Shared Subgraph Lowering

The frontend now keeps shared conversion state across the main graph and
referenced subgraphs:

- `lowered_subgraphs`
- `lowered_if_functions`
- `lowered_while_functions`
- `lowering_stack`
- `module_builder`

Referenced pure tensor subgraphs are lowered through a recursive
`OperatorConverter` using an isolated `ExprTable`, so subgraph tensor
bindings
cannot overwrite bindings from the main graph. Lowered subgraphs are
cached by
subgraph index and reused when the same region is referenced more than
once.
Generated private functions are registered through the shared parent
`module_builder`, so nested cases such as `main CALL -> subgraph A ->
CALL
subgraph B` keep all private functions in the final IRModule.

Recursive ordinary `CALL` subgraphs are guarded with `OpNotImplemented`.
`WHILE` uses a dedicated recursive wrapper function instead, because
recursion
is part of the intended Relax representation for the loop itself.

### Boundary Validation

The control-flow converters validate subgraph boundaries before
lowering:

- referenced subgraph indices must be valid
- op input/output arity must match the referenced subgraph interface
- branch and loop tensor shape/dtype metadata must match the surrounding
op
- `IF` and `WHILE` conditions must be scalar bool tensors
- `WHILE` loop-carried input/output tensors must have matching metadata

The shared `_check_subgraph_interface` helper is used by `CALL`, `IF`,
and
`WHILE` to keep arity and metadata checks consistent across the
control-flow
operators. `_require_scalar_bool_tensor` accepts both frontend
`TensorWrapper`
objects and raw TFLite tensors so caller and referenced-subgraph
condition
checks use the same path.

These checks keep the first implementation conservative and make
unsupported
cases fail with targeted `OpNotImplemented` diagnostics.

### Tuple Outputs

TFLite `CALL`, `IF`, and `WHILE` may produce multiple output tensors.
The
frontend maps those cases to Relax tuple returns:

```text
single output  -> tensor expression
multi output   -> Tuple(...)
op outputs     -> TupleGetItem(...)
```

This keeps the single-output IR simple while covering multi-output
calls,
multi-output branches, and multi-variable loop state.

## Operator Support

| Operator | TFLite options | Relax lowering | Supported subset |
|---|---|---|---|
| `CALL` | `CallOptions.Subgraph()` | private Relax function call | pure
tensor subgraphs, single or multiple outputs |
| `IF` | `IfOptions.ThenSubgraphIndex()`, `ElseSubgraphIndex()` |
private wrapper function containing Relax `If` | scalar bool condition,
matching branch I/O metadata |
| `WHILE` | `WhileOptions.CondSubgraphIndex()`, `BodySubgraphIndex()` |
recursive private Relax function | scalar bool cond output, tensor
loop-carried state |
| `CALL_ONCE` | `CallOnceOptions.InitSubgraphIndex()` | no-op for empty
init subgraph | empty init subgraph only |

## Not Included

- Full `CALL_ONCE` resource/variable initialization semantics.
- Resource, variant, hashtable, or variable tensor support.
- TensorFlow-generated `tf.cond` / `tf.while_loop` smoke tests.
- Dynamic-shape loop-state refinements beyond the current static
metadata
  checks.

## Tests

The tests manually build minimal TFLite flatbuffers and compare the
imported
Relax IR with `tvm.ir.assert_structural_equal`. Unsupported-boundary
tests use
`pytest.raises`.

| Test | Coverage |
|---|---|
| `test_call_subgraph` | basic `CALL` to a pure tensor subgraph |
| `test_call_subgraph_multi_output` | `CALL` tuple return and output
binding |
| `test_call_subgraph_nested_call` | nested `CALL` private function
registration |
| `test_call_subgraph_invalid_index_unsupported` | invalid `CALL`
subgraph index |
| `test_call_subgraph_io_mismatch_unsupported` | `CALL` arity mismatch |
| `test_call_subgraph_output_metadata_mismatch_unsupported` | `CALL`
output metadata guard |
| `test_if_subgraphs` | basic `IF` branch selection |
| `test_if_subgraphs_multi_output` | `IF` tuple branch returns |
| `test_if_subgraphs_non_bool_condition_unsupported` | `IF` condition
dtype guard |
| `test_if_subgraphs_invalid_index_unsupported` | invalid then/else
subgraph index |
| `test_if_subgraphs_output_count_mismatch_unsupported` | branch output
count guard |
| `test_if_subgraphs_input_metadata_mismatch_unsupported` | branch input
metadata guard |
| `test_if_subgraphs_output_metadata_mismatch_unsupported` | branch
output metadata guard |
| `test_while_subgraphs` | basic recursive `WHILE` lowering |
| `test_while_subgraphs_repeated_cond_body_pair` | shared cond/body loop
function cache |
| `test_while_subgraphs_two_loop_vars` | multi-variable loop state tuple
path |
| `test_while_subgraphs_non_bool_condition_unsupported` | `WHILE` cond
output dtype guard |
| `test_while_subgraphs_invalid_index_unsupported` | invalid cond/body
subgraph index |
| `test_while_subgraphs_zero_loop_vars_unsupported` | zero-loop-var
guard |
| `test_while_subgraphs_loop_state_metadata_mismatch_unsupported` | loop
state metadata guard |
| `test_while_subgraphs_output_count_mismatch_unsupported` | body output
count guard |
| `test_while_subgraphs_input_metadata_mismatch_unsupported` | cond/body
input metadata guard |
| `test_while_subgraphs_output_metadata_mismatch_unsupported` |
cond/body output metadata guard |
| `test_call_once_empty_init_subgraph` | empty `CALL_ONCE` no-op subset
|
| `test_call_once_non_empty_init_subgraph_unsupported` | non-empty init
subgraph guard |
| `test_call_once_inputs_outputs_unsupported` | `CALL_ONCE` op I/O guard
|
| `test_call_once_init_subgraph_io_unsupported` | init subgraph I/O
guard |
| `test_call_once_invalid_index_unsupported` | invalid init subgraph
index |

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 pytest \
  tests/python/relax/test_frontend_tflite.py \
  -k "call_subgraph or if_subgraphs or while_subgraphs or call_once" -q

python -m pytest \
  tests/python/relax/test_frontend_tflite.py -q
```

Result:

```text
ruff format --check: 2 files already formatted
ruff check: All checks passed
28 passed, 434 deselected
462 passed
```

## References

- Issue #19519 item F: TFLite control-flow / multi-subgraph operators
- PR #19587: StableHLO region-based ops and multi-subgraph model support
tlopex pushed a commit that referenced this pull request May 27, 2026
…19601)

## Summary

This PR adds Relax TFLite frontend support for
`UNIDIRECTIONAL_SEQUENCE_RNN` (BuiltinOperator 35), claimed in
[#19519](#19519) Group A.

The op executes a simple RNN cell over a time sequence. The converter
unrolls the time steps at graph-construction time using Relax
primitives.

Cell equation:
```
h_t = fused_activation(x_t @ W.T + h_{t-1} @ Wr.T + b)
```

## Changes

- **Handler**: `convert_unidirectional_sequence_rnn` registered in
`convert_map` (alphabetical, U-region after `UNPACK`)
- **Inputs** (5): `input [batch, time, input_size]`, `input_weights
[num_units, input_size]`, `recurrent_weights [num_units, num_units]`,
`bias [num_units]`, `hidden_state [batch, num_units]` (variable,
zero-initialised)
- **Output**: `[batch, time, num_units]` (always batch-major)
- **time_major=True**: input is transposed to batch-major before
unrolling
- **Activations**: NONE, RELU, RELU6, TANH, SIGMOID (via
`convert_fused_activation_function`)
- **Quantized**: raises `OpNotImplemented` (not yet supported)

## Testing

Modern TF/Keras (2.x, Keras 3) no longer emits
`UNIDIRECTIONAL_SEQUENCE_RNN`; `SimpleRNN` with `unroll=False` lowers to
`WHILE`+TensorList ops, and `unroll=True` expands to elementwise ops.
Tests therefore follow the same flatbuffer-construction pattern used by
the StableHLO op PRs (#19536, #19587).

Three tests added to `tests/python/relax/test_frontend_tflite.py`:

- `test_unidirectional_sequence_rnn_none_activation` —
`tvm.ir.assert_structural_equal` with identity weights / zero bias, NONE
activation, time=1
- `test_unidirectional_sequence_rnn_relu_activation` — shape check,
random weights, RELU activation, time=3
- `test_unidirectional_sequence_rnn_time_major` — shape check,
`time_major=True` input layout

```bash
python -m pytest tests/python/relax/test_frontend_tflite.py -k unidirectional_sequence_rnn -v
```

All 3 tests pass. pre-commit (ASF header, ruff check, ruff format) all
pass.

## References

- Issue [#19519](#19519) Group A:
Sequence / recurrent model operators

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tlopex pushed a commit that referenced this pull request May 31, 2026
## Summary

This PR adds Relax TFLite frontend support for the TFLite builtin
`STABLEHLO_WHILE` operator.

`STABLEHLO_WHILE` uses StableHLO `BuiltinOptions2` to reference its
condition
and body region subgraphs. Its loop semantics otherwise match the
existing
TFLite `WHILE` importer path: loop-carried tensors are passed to the
cond/body
subgraphs, the cond subgraph returns a scalar bool, and the body
subgraph
returns the updated loop state.

## Design

### Shared While Lowering

The native TFLite `WHILE` converter is refactored through a shared
`_convert_while_like` helper. Native `WHILE` and `STABLEHLO_WHILE` now
share the
same validation and lowering path after their options are parsed:

- native `WHILE` reads `WhileOptions` from `BuiltinOptions`
- `STABLEHLO_WHILE` reads `StablehloWhileOptions` from `BuiltinOptions2`

Both paths lower the referenced cond/body subgraphs to private Relax
functions
and emit a recursive private Relax function for the loop.

### Boundary Validation

`STABLEHLO_WHILE` reuses the same guard-first checks as native `WHILE`:

- loop input count must match op output count
- cond subgraph input metadata must match loop-carried tensors
- cond subgraph must have exactly one output
- cond output must be a scalar bool tensor
- body subgraph input and output metadata must match loop-carried
tensors
- referenced cond/body subgraph indices must be valid non-main subgraphs

The recursive loop-function cache key now includes the generated
function
prefix. This prevents native `WHILE` and `STABLEHLO_WHILE` from
accidentally
sharing a cached loop wrapper if they reference the same cond/body
subgraph
indices.

## Operator Support

| Operator | TFLite options | Relax lowering | Supported subset |
|---|---|---|---|
| `STABLEHLO_WHILE` | `StablehloWhileOptions.CondSubgraphIndex()`,
`BodySubgraphIndex()` from `BuiltinOptions2` | recursive private Relax
function | tensor loop-carried state, scalar bool cond output, matching
cond/body interfaces |

## Tests

The tests manually build a minimal StableHLO while TFLite flatbuffer and
compare
the imported Relax IR with `tvm.ir.assert_structural_equal`. Unsupported
patterns use `pytest.raises`.

| Test | Coverage |
|---|---|
| `test_stablehlo_while` | basic `STABLEHLO_WHILE` recursive private
function lowering |
| `test_stablehlo_while_non_bool_condition_unsupported` | cond output
scalar bool guard |
| `test_stablehlo_while_invalid_index_unsupported` | invalid cond/body
subgraph index guard |
| `test_stablehlo_while_output_count_mismatch_unsupported` | body output
arity guard |
| `test_stablehlo_while_input_metadata_mismatch_unsupported` | cond
subgraph input metadata guard |
| `test_stablehlo_while_output_metadata_mismatch_unsupported` | body
subgraph output metadata guard |

Local validation:

```bash
python -m py_compile \
  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 pytest \
  tests/python/relax/test_frontend_tflite.py \
  -k stablehlo_while -q

python -m pytest \
  tests/python/relax/test_frontend_tflite.py \
  -k stablehlo -q
```

Result:

```text
py_compile: passed
ruff check: All checks passed
stablehlo_while tests: 6 passed
stablehlo tests: 84 passed
```

## References

- Issue #19519 item I: remaining StableHLO operators in TFLite
- PR #19587: StableHLO region-based ops and multi-subgraph model support
- PR #19616: TFLite control-flow / multi-subgraph support
tlopex pushed a commit that referenced this pull request Jun 25, 2026
…#19881)

## Summary

This PR adds Relax TFLite frontend support for runtime (dynamic) start
indices
in `STABLEHLO_DYNAMIC_UPDATE_SLICE`, addressing the
`DYNAMIC_UPDATE_SLICE` item
from #19412 section B.

`_convert_stablehlo_dynamic_update_slice` (added in #19587) previously
raised
`OpNotImplemented` when the start-index scalars were runtime
(non-constant)
values, handling only compile-time-constant starts. Models that compute
the
update offset at runtime could therefore not be imported. This PR makes
the
dynamic-start path work, with StableHLO clamping semantics, without
adding a new
Relax op. The change is limited to this converter and its test.

## Design

### Dynamic start indices via scatter_nd

The existing static path already lowers `STABLEHLO_DYNAMIC_UPDATE_SLICE`
to
`relax.op.scatter_nd`, building the scatter index grid at compile time
with
`numpy.indices`. `scatter_nd` accepts a general **runtime** `indices`
tensor and
returns the `data` (operand) shape unchanged, so the dynamic case needs
no new
op and introduces no symbolic dimensions — only the index grid is built
in-graph instead of in NumPy.

For runtime starts, the converter builds the index grid per axis `a`
(rank is
statically known from the operand/update shapes):

- clamp the start to `[0, operand_dim - update_dim]` with
`relax.op.maximum` /
`relax.op.minimum` — StableHLO clamps out-of-range starts rather than
erroring;
- `idx = arange(update_dim) + clamped_start`;
- reshape `idx` to broadcast on axis `a` and `broadcast_to` the update
shape;
- `expand_dims` a trailing index axis.

`concat` over the axes produces an int64 index tensor of shape
`(*update_shape, rank)`, which is fed to the same
`relax.op.scatter_nd(operand, indices, update, "update")` call the
static path
uses.

The static (constant-start) path is unchanged, including its
compile-time
out-of-bounds rejection.

## Operator Support

| Operator | TFLite inputs | Relax lowering | Supported subset |
|---|---|---|---|
| `STABLEHLO_DYNAMIC_UPDATE_SLICE` | `operand`, `update`, N scalar
`start` indices | `relax.op.scatter_nd` with a NumPy index grid
(constant starts) or an in-graph `arange` + clamp index grid (runtime
starts) | static operand/update shapes; constant or runtime start
indices |

## Not Included

- Dynamic (non-static) operand or update shapes — the index grid is
built from
the statically known update shape, so operand/update shapes must be
static.
Runtime *start indices* are supported; runtime *tensor shapes* are not.

## Tests

The dynamic-start test compiles the imported module and runs it on the
Relax VM,
comparing the output against a NumPy reference; it includes an
out-of-range start
to exercise clamping. The static structural-equal and out-of-bounds
tests are
unchanged.

| Test | Coverage |
|---|---|
| `test_stablehlo_dynamic_update_slice` | constant start indices,
structural-equal (existing) |
| `test_stablehlo_dynamic_update_slice_dynamic_starts` | runtime start
indices, compile + run, including an out-of-range start that is clamped
|
| `test_stablehlo_dynamic_update_slice_out_of_bounds_unsupported` |
constant-start path rejects out-of-bounds updates (existing) |

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 pytest \
  tests/python/relax/test_frontend_tflite.py -k dynamic_update_slice -q

python -m pytest \
  tests/python/relax/test_frontend_tflite.py -q
```

Result:

```text
ruff format --check: 2 files already formatted
ruff check: All checks passed
dynamic_update_slice tests: 3 passed, 555 deselected
full TFLite pytest: 558 passed
```

## References

- Issue #19412 section B: `DYNAMIC_UPDATE_SLICE`
- PR #19587: introduced `STABLEHLO_DYNAMIC_UPDATE_SLICE` (constant
starts) and
  multi-subgraph / StableHLO region support
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants