[Relax][Frontend][TFLite] Add missing TFLite operator mappings#19813
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses several Relax TFLite frontend gaps by implementing mappings for operators such as SIGN, BITWISE_XOR, RIGHT_SHIFT, BUCKETIZE, RELU_0_TO_1, UNIQUE, RANK, UNSORTED_SEGMENT_SUM, and UNSORTED_SEGMENT_MAX, along with fixing FAKE_QUANT narrow-range vector conversion. Feedback suggests simplifying the RANK conversion by passing the Python integer directly to relax.const rather than wrapping it in a NumPy array, and using positional arguments instead of keyword arguments in relax.op.clip for consistency and compatibility.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| output_dtype = self.get_tensor_type_str(output_tensors[0].tensor.Type()) | ||
|
|
||
| rank = len(self.get_tensor_shape(input_tensors[0])) | ||
| return relax.const(np.array(rank, dtype=output_dtype), dtype=output_dtype) |
There was a problem hiding this comment.
We can simplify this by passing the Python integer rank directly to relax.const instead of wrapping it in a np.array. relax.const natively supports Python scalars with a specified dtype.
| return relax.const(np.array(rank, dtype=output_dtype), dtype=output_dtype) | |
| return relax.const(rank, dtype=output_dtype) |
There was a problem hiding this comment.
Addressed by returning relax.const(rank, dtype=output_dtype).
| if input_tensor.qnn_params: | ||
| in_f32 = self.dequantize(in_expr, input_tensor) | ||
| out = relax.op.clip(in_f32, min=0, max=1) | ||
| out = self.quantize(out, output_tensor) | ||
| else: | ||
| out = relax.op.clip(in_expr, min=0, max=1) |
There was a problem hiding this comment.
For consistency with other uses of relax.op.clip in this file (e.g., in convert_fake_quant), and to ensure compatibility with TVM's FFI wrappers, it is recommended to use positional arguments instead of keyword arguments (min and max).
| if input_tensor.qnn_params: | |
| in_f32 = self.dequantize(in_expr, input_tensor) | |
| out = relax.op.clip(in_f32, min=0, max=1) | |
| out = self.quantize(out, output_tensor) | |
| else: | |
| out = relax.op.clip(in_expr, min=0, max=1) | |
| if input_tensor.qnn_params: | |
| in_f32 = self.dequantize(in_expr, input_tensor) | |
| out = relax.op.clip(in_f32, 0, 1) | |
| out = self.quantize(out, output_tensor) | |
| else: | |
| out = relax.op.clip(in_expr, 0, 1) |
There was a problem hiding this comment.
Addressed by switching the new relax.op.clip calls to positional arguments.
5843680 to
864935f
Compare
|
Could you resolve the conflict so that we can merge it in? |
c786073 to
0ff302e
Compare
|
Rebased the PR onto the latest Also refreshed the PR description with the post-rebase validation results. |
|
Updated the PR description for clearer scope: the changes are now grouped by the |
## Summary This PR adds Relax TFLite frontend support for dynamic (runtime) scalar bounds in the `RANGE` operator, addressing the `RANGE` "fix partial implementations" item from #19412 section C. `convert_range` previously lowered only **constant** `start`, `limit`, and `delta` to `relax.op.arange` and raised `OpNotImplemented` for runtime scalar bounds (the guard added in #19401). Models that compute RANGE bounds at runtime could therefore not be imported. This PR makes the dynamic path work for both integer and float bounds, ascending or descending, without adding a new Relax op. The change is limited to the `RANGE` converter and its test. #19813 added a batch of missing TFLite operator mappings but did not touch this partial-implementation item; this PR closes it. ## Design ### Dynamic scalar bounds via count-lift `relax.op.arange` only accepts compile-time `PrimExpr` bounds. The frontend already has a runtime-scalar -> symbolic-dimension bridge (`relax.op.tensor_to_shape` + `match_cast`, as used by `_get_shape_expr_from_tensor`), so no new op is needed. Rather than feed symbolic bounds straight into `arange`, the converter computes the element **count** in-graph and lifts that single value to one symbolic output dimension `L`, then rebuilds the values as `arange(0, L) * delta + start`. Lifting the count (instead of the bounds) keeps the declared and runtime output lengths equal by construction: `arange`'s struct-info length formula (`InferTypeArange`) has no negative-step branch, so feeding symbolic bounds directly would mis-declare descending ranges relative to the TOPI runtime length. The count is `max(0, ceil((limit - start) / delta))`, computed per dtype: - **integer**: `-floor_divide(start - limit, delta)` — exact, sign-agnostic, and free of float-precision loss; equal to `ceil((limit - start) / delta)`. - **float**: `ceil((limit - start) / delta)`. Constant (all-bounds-constant) RANGE keeps the existing direct-`arange` path unchanged. ## Operator Support | Operator | TFLite inputs | Relax lowering | Supported subset | |---|---|---|---| | `RANGE` | scalar `start`, `limit`, `delta` | `relax.op.arange` (constant bounds); count-lift + `arange(0, L) * delta + start` (dynamic bounds) | int and float, constant or runtime scalar bounds, ascending or descending | ## Tests The dynamic test compiles the imported module and runs it on the Relax VM, comparing the output against `numpy.arange`. The constant-bound structural test is unchanged. | Test | Coverage | |---|---| | `test_range` | constant scalar bounds (existing, unchanged) | | `test_range_dynamic_scalar_inputs` | runtime scalar bounds: int and float, ascending and descending | 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 range -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 range tests: 12 passed, 536 deselected full TFLite pytest: 548 passed ``` ## References - Issue #19412 section C: fix partial TFLite operator implementations (`RANGE`) - PR #19401: added the `RANGE` dynamic-scalar guard and its test - PR #18868: introduced the Relax TFLite frontend and `convert_range`
Summary
Adds Relax TFLite frontend coverage for a batch of builtin operators tracked in
#19412. All lowerings reuse existing Relax ops; the change is limited to the
TFLite frontend converter and its tests.
Changes
Grouped by the #19412 checklist section:
Section A — drop-in operator mappings
SIGN->relax.op.signBITWISE_XOR->relax.op.bitwise_xorRIGHT_SHIFT->relax.op.right_shiftRELU_0_TO_1->relax.op.clipBUCKETIZE->relax.op.bucketizeUNIQUE->relax.op.unique(unique values + inverse indices)Section B — compose with existing Relax ops
RANK-> scalar rank constant for statically-ranked inputsUNSORTED_SEGMENT_SUM-> extends the existing scatter-based segment lowering(
scatter_nd, reduction="add")UNSORTED_SEGMENT_MAX-> same, reduction="max"Section C — fix partial implementation
FAKE_QUANT: the handler called the non-existentrelax.op.constand raisedAttributeErroron every invocation; switch torelax.constso the op works.(The builtin op carries only scalar min/max, so there is no per-channel/vector
min/max path.)
Tests
Adds coverage in
tests/python/relax/test_frontend_tflite.pyfor the newmappings and the
FAKE_QUANTnarrow-range vector path. Most tests usestructural-equal Relax IR checks; data-dependent and numeric regression cases
add focused output checks for
UNIQUEandFAKE_QUANT.python -m ruff format 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 "unique or sign or bitwise_xor or right_shift or bucketize or relu_0_to_1 or rank or unsorted_segment_sum or unsorted_segment_max or fake_quant" python -m pytest tests/python/relax/test_frontend_tflite.pyResult:
References