Skip to content

[Relax][Frontend][TFLite] Support static hashtable find - #19879

Merged
tlopex merged 1 commit into
apache:mainfrom
Aharrypotter:tflite-hashtable-find
Jun 24, 2026
Merged

tlopex merged 1 commit into
apache:mainfrom
Aharrypotter:tflite-hashtable-find

Conversation

@Aharrypotter

Copy link
Copy Markdown
Contributor

Summary

This PR extends the TFLite resource hashtable family from #19519 item G from
"import + size" to a constant-foldable HASHTABLE_FIND subset. It builds on the
static HASHTABLE / HASHTABLE_IMPORT import support added in #19639 and the
HASHTABLE_LOOKUP converter from #19654.

Relax has no string tensor type and no runtime hashtable operator, so this PR
targets the only subset that can be lowered today: tables whose keys and values
are constants imported in a CALL_ONCE init subgraph, queried by a constant
string tensor. In that case the lookup is resolved at import time and emitted as
a relax.const. Runtime string queries remain explicitly guarded instead of
being lowered with incorrect semantics.

Design

Static String Tensor Decoding

The frontend now decodes constant TFLite string tensors through a shared helper
_get_string_tensor_value. It parses the TFLite string-tensor binary layout:

[count(i32)][offset_0 .. offset_count(i32)][utf8 string data]

The helper validates the buffer length against the declared count, checks that
offsets are in-bounds and monotonically non-decreasing, and verifies the decoded
element count matches the tensor shape. This is reusable infrastructure for any
future TFLite string support, independent of HASHTABLE_FIND.

Hashtable Import State

HASHTABLE_IMPORT now stores the actual constant keys and values (numeric or
decoded string buffers) in shared conversion state, instead of only recording
table metadata (size, key/value dtype). Duplicate keys are rejected, because the
constant-fold lookup assumes a unique key mapping. The captured table is keyed by
the same table_id / handle resolution used by HASHTABLE and HASHTABLE_SIZE,
so a CALL_ONCE init subgraph and the main graph agree on the same logical
table.

Constant-Foldable Find

HASHTABLE_FIND resolves the table handle through the importer-local handle map
and the statically imported keys/values. For the supported subset it builds a
Python key -> value map, applies the per-element default value, overwrites hits
from the table, preserves the query tensor shape, and emits the result as a
relax.const. The op produces no runtime Relax computation, which matches the
fact that both the table and the query are compile-time constants.

The supported subset is intentionally narrow and guard-first:

  • the table must be a constant string -> int64 table imported via a supported
    CALL_ONCE HASHTABLE_IMPORT
  • the query tensor must be a constant string buffer
  • the default tensor must be a scalar or match the query shape

String Graph Input Guard

TensorType.STRING graph inputs are now rejected in from_tflite with a clear
OpNotImplemented instead of a low-level FFI unknown dtype string error, since
Relax cannot represent a string tensor. This is the path hit by runtime string
queries for HASHTABLE_FIND and string-valued HASHTABLE_LOOKUP, so the guard
gives a frontend-level diagnostic for both.

Operator Support

Operator TFLite options Relax lowering Supported subset
HASHTABLE_IMPORT HashtableImportOptions store constant keys/values in importer state CALL_ONCE init, constant keys/values, no duplicate keys
HASHTABLE_FIND HashtableFindOptions constant-fold to relax.const constant string -> int64 table + constant string query

Not Included

  • Runtime (non-constant) string queries and runtime hashtable lookup.
  • int64 -> string find, which would require string-typed Relax outputs.
  • Any general TensorType.STRING tensor representation in Relax.
  • A runtime Relax hashtable operator with string hashing / comparison.
  • Mutable runtime resource-state threading through Relax functions.

These require core Relax support and are out of scope for the frontend.

Tests

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

Test Coverage
test_hashtable_call_once_import_find_string_to_int64 constant string -> int64 find folds to a relax.const
test_hashtable_call_once_import_find_string_to_int64_2d_query query shape preserved for a 2-D query
test_hashtable_call_once_import_find_int64_to_string_unsupported int64 -> string table rejected
test_hashtable_call_once_import_find_runtime_query_unsupported runtime string query rejected
test_hashtable_call_once_import_duplicate_keys_unsupported duplicate static keys rejected
test_hashtable_lookup_string_value_unsupported string graph input now gives a clean OpNotImplemented

Local validation:

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 "hashtable or resource or variable" -q

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

Result:

ruff format --check: 2 files already formatted
ruff check: All checks passed
14 passed, 535 deselected
549 passed

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 adds support for constant-foldable string-to-int64 HASHTABLE_FIND operations in the Relax TFLite frontend, including decoding constant string tensor buffers and handling HASHTABLE_IMPORT with string keys or values. Feedback focuses on improving robustness and efficiency: catching potential UnicodeDecodeError when decoding string tensors, using np.unique to check for duplicate keys more efficiently, and supporting 1-element default value tensors in HASHTABLE_FIND instead of only strict scalars.

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.

Comment thread python/tvm/relax/frontend/tflite/tflite_frontend.py Outdated
Comment thread python/tvm/relax/frontend/tflite/tflite_frontend.py Outdated
Comment thread python/tvm/relax/frontend/tflite/tflite_frontend.py Outdated
Add a constant-foldable subset for the TFLite resource hashtable family,
building on the static HASHTABLE / HASHTABLE_IMPORT import support.

- Add a TFLite string-tensor buffer decoder (_get_string_tensor_value)
  that parses the [count][offsets][utf8 data] layout with bounds and
  monotonicity validation. This is reusable infrastructure for any future
  string support.
- HASHTABLE_IMPORT now decodes and stores constant keys/values (including
  string buffers) and rejects duplicate keys, instead of only recording
  table metadata.
- HASHTABLE_FIND now supports the constant string->int64 table subset by
  folding the lookup to a relax.const. Runtime string queries, int64->string
  tables, and non-constant tables remain explicitly unsupported.
- Reject TensorType.STRING graph inputs in from_tflite with a clear
  OpNotImplemented instead of a low-level FFI 'unknown dtype string' error.
  This covers runtime string queries for HASHTABLE_FIND/HASHTABLE_LOOKUP.

Relax still has no string tensor type or runtime hashtable op, so general
runtime string-keyed lookups remain out of scope.
@Aharrypotter
Aharrypotter force-pushed the tflite-hashtable-find branch from c75c496 to 9732138 Compare June 24, 2026 03:27
@Aharrypotter

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 adds support for constant-folding a subset of HASHTABLE_FIND operations (specifically constant string-to-int64 tables) in the Relax TFLite frontend. It introduces a helper method to decode constant TFLite string tensor buffers and updates convert_hashtable_import and convert_hashtable_find accordingly, while raising errors for unsupported cases like runtime string queries. Feedback is provided to add shape validation between keys and values in HASHTABLE_IMPORT to prevent silent truncation during lookup.

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.

Comment on lines +810 to +813
if np.unique(keys).size != keys.size:
raise tvm.error.OpNotImplemented(
"HASHTABLE_IMPORT with duplicate keys is not supported"
)

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.

medium

In HASHTABLE_IMPORT, there is no validation to ensure that the keys and values tensors have matching shapes. If a model has mismatched key and value shapes, zip in convert_hashtable_find will silently truncate the elements, leading to unexpected behavior or incorrect constant folding. We should explicitly validate that the shapes of keys and values match before proceeding.

        if keys.shape != values.shape:
            raise tvm.error.OpNotImplemented(
                "HASHTABLE_IMPORT requires keys and values to have the same shape"
            )

        if np.unique(keys).size != keys.size:
            raise tvm.error.OpNotImplemented(
                "HASHTABLE_IMPORT with duplicate keys is not supported"
            )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This case already be covered by the existing import-time guard just above
this block:

key_shape = self._get_tensor_shape_tuple(key_tensor)
value_shape = self._get_tensor_shape_tuple(value_tensor)
if key_shape != value_shape:
    raise tvm.error.OpNotImplemented("HASHTABLE_IMPORT requires keys and values same shape")

This runs before the keys and values are stored for HASHTABLE_FIND, so a
mismatched import should be rejected before the later zip path can silently
truncate anything. I do not think an extra keys.shape != values.shape check
changes the frontend behavior here.

@tlopex tlopex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@tlopex
tlopex merged commit 5a8dae4 into apache:main Jun 24, 2026
12 checks passed
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