Skip to content

recipe: tflite-runtime 2.21.0#105

Open
ndonkoHenri wants to merge 11 commits into
mainfrom
machine/tflite-runtime
Open

recipe: tflite-runtime 2.21.0#105
ndonkoHenri wants to merge 11 commits into
mainfrom
machine/tflite-runtime

Conversation

@ndonkoHenri

Copy link
Copy Markdown

Adds a tflite-runtime recipe — LiteRT / .tflite model inference on both Android and iOS.
Closes flet-dev/flet#5135 and flet-dev/flet#3810 (and relevant to flet-dev/flet#4901).

Shape

tflite-runtime has never had an sdist — upstream wheels come from tensorflow/lite/tools/pip_package/build_pip_package_with_cmake.sh, a host==target script. So this uses the PEP 517 shim archetype (same as onnxruntime): the source is the TensorFlow v2.21.0 release tree, and mobile.patch adds a pyproject.toml + _forge/forge_tflite_backend.py that builds a standalone host flatc, cross-compiles tensorflow/lite down to just the _pywrap_tensorflow_interpreter_wrapper pybind module, then stages upstream's exact 5-file tflite_runtime/ package. Wheels are 1.9–2.8 MB.

2.21 is the last buildable line — the ai-edge-litert successor is a Bazel wall (no CMake path), so pinning here is deliberate. Both historical p4a patches (-llog, minimal_logging dedup) are already fixed upstream in 2.21.

Notable cross-compile details (each documented in the recipe/patch):

  • -DTENSORFLOW_SOURCE_DIR pinned to the unpacked root — unset, lite/'s CMake git-clones its own TF at v2.19.0 and compiles 2.21 sources against 2.19 headers.
  • XNNPACK is link-mandatory for the Python wrapper on every ABI (the wrapper references TfLiteXNNPackDelegate* unconditionally); stays ON except 32-bit arm, following upstream's own pip convention.
  • iOS (3 tells): wrapper has no FindPython → -Wl,-undefined,dynamic_lookup (Android links libpython explicitly instead); FetchContent'd flatbuffers' flatc install rejected by iOS's forced MACOSX_BUNDLE → -DFLATBUFFERS_BUILD_FLATC=OFF -DFLATBUFFERS_INSTALL=OFF (host flatc does codegen); apple SHARED wrapper is a .dylib → staged under the .so name.

Testing

tests/test_tflite_runtime.py runs a committed 1 KB dense_relu.tflite (generated and validated with desktop TF 2.21, fixed weights + seed 42) through a real Interpreter.invoke() — the CI emulator/simulator runs actual XNNPACK inference and asserts desktop-recorded outputs to 1e-6, plus a re-set/second-invoke test. Test logic was pre-validated on desktop TF via a tflite_runtime.interpretertf.lite module alias, which caught a real numeric bug before any device.

Follow-ups (not in this PR)

Delegates beyond XNNPACK (GPU / NNAPI / Core ML) if there's demand — the default XNNPACK-accelerated CPU path is the portable baseline.

Consumer notes (usage & recommendations)

tflite-runtime runs .tflite models through the XNNPACK-accelerated CPU path — the portable, dependable baseline that works identically on Android and iOS. Everything runs offline; the only network you need is a one-time model download (or bundle the model — see below). The pinned version is 2.21.0, the last buildable release — its ai-edge-litert successor can't be cross-compiled, so stay on this package name.

Install

Add it to your deps (the pip name has a hyphen, the import has an underscore):

# pyproject.toml
dependencies = [
    "flet",
    "tflite-runtime",
    "numpy",
]

All Android ABIs are covered (arm64-v8a, armeabi-v7a, x86_64) — though on old 32-bit armeabi-v7a the XNNPACK accelerator is off, so prefer arm64 devices.

Storage & the model file

.tflite models are usually small, so you have two options:

  • Small model (a few MB): ship it along with your app (ex: in the assets/ directory) and load from the bundle — simplest.
  • Larger model: don't bloat the app bundle — download it once on first run into Flet's persistent storage (FLET_APP_STORAGE_DATA) and load from there:
import os, urllib.request

data_dir = os.getenv("FLET_APP_STORAGE_DATA")
model_path = os.path.join(data_dir, "model.tflite")
if not os.path.exists(model_path):
    urllib.request.urlretrieve("https://<your-host>/model.tflite", model_path)

Minimal usage

import os
import numpy as np
from tflite_runtime.interpreter import Interpreter

model_path = os.path.join(os.getenv("FLET_APP_STORAGE_DATA"), "model.tflite")

# num_threads: set to your device's performance-core count (often 4).
interpreter = Interpreter(model_path=model_path, num_threads=4)
interpreter.allocate_tensors()

inp = interpreter.get_input_details()[0]
out = interpreter.get_output_details()[0]

x = np.asarray(data, dtype=np.float32)      # match inp["shape"] / inp["dtype"]
interpreter.set_tensor(inp["index"], x)
interpreter.invoke()
y = interpreter.get_tensor(out["index"])

Run it off the UI thread

allocate_tensors() and invoke() are CPU-bound — run them in page.run_thread(...) so the UI stays responsive.

Notes for good results on mobile

  • Quantized (INT8) models feed/read raw uint8/int8, not float — use the inp["quantization"] (scale, zero-point) to convert. They're smaller and faster, ideal for mobile.
  • Dynamic input shapes: call interpreter.resize_tensor_input(inp["index"], new_shape) then allocate_tensors() again before setting the tensor.
  • XNNPACK is applied automatically for float models — no delegate setup needed.
  • You may see a cosmetic warning about ai_edge_litert; ignore it (that successor isn't buildable for mobile, which is why we pin this package).

No sdist has ever existed (upstream wheels come from
tensorflow/lite/tools/pip_package/build_pip_package_with_cmake.sh, whose
cmake dispatch has no android case; python-for-android patches one in on
a TF 2.8 pin). Build from the TF v2.21.0 tag - the legacy CMake pip path
is still intact there and tf.lite has not yet moved out to LiteRT.

mobile.patch ADDS three files (TF root ships neither pyproject.toml nor
setup.py, nothing replaced): a PEP 517 shim (host-flatc build + cross
cmake of tensorflow/lite building ONLY the pybind wrapper target +
upstream's exact 5-file tflite_runtime/ staging) and an adapted
setup_with_binary.py. The wrapper has no FindPython: Python/numpy/pybind11
headers ride -I injections ({platlib} paths, no target-numpy imports);
libpython is linked explicitly (Android wants the DT_NEEDED; upstream
relies on manylinux lazy resolution).

Three fights, all captured in the patch/shim comments:
- TENSORFLOW_SOURCE_DIR must be pinned to the unpacked root: unset, the
  lite build git-clones ITS OWN TensorFlow at v2.19.0 and compiles this
  tree's sources against those stale headers (first slice died on a
  header that only exists in 2.21).
- TF's Bazel `BUILD` file collides with setuptools' `build/` dir on
  case-insensitive filesystems (macOS local builds) -> build_base moved.
- XNNPACK cannot be turned off for the python wheel on ANY ABI:
  interpreter_wrapper.cc references TfLiteXNNPackDelegate* at link time.
  Upstream's own 32-bit-arm OFF convention only works for the C++ lib;
  XNNPACK-on-v7a builds fine with NDK r27 (p4a shipped it at TF 2.8).

Host flatc: standalone build of the exact flatbuffers tag TF pins
(parsed from tools/cmake/modules/flatbuffers.cmake), cached in the temp
dir per tag - host-arch tool, safe to share across ABI slices, and much
cheaper than upstream's second full lite configure.

Local matrix 3/3 (arm64-v8a, x86_64 - the p4a-era x86_64 failure does
not reproduce on 2.21 - and armeabi-v7a). Wheels 1.9-2.8MB, upstream's
exact package layout, 16KB-aligned, minimal DT_NEEDED. On-device arm64
emulator: 4/4 PASSED incl. REAL inference through XNNPACK on a 1KB
committed dense+relu model (desktop-recorded expectations matched
exactly; tests/requirements.txt pulls numpy - the wheel's one runtime
dep). tf.lite.Interpreter deprecation warning is upstream cosmetics;
ai-edge-litert (the successor) is Bazel-only and stays infeasible.
…solution logic and add platform-specific linker flag adjustments.
…atform parity

Same playbook as the onnxruntime-iOS extension: jinja lane split in
FORGE_CMAKE_ARGS (apple toolchain args) and per-slice apple keying in the
shim (sysroot+arch), plus three iOS-specific pieces:
- FORGE_SHARED_LINKER_FLAGS = -Wl,-undefined,dynamic_lookup: the wrapper
  target has no FindPython to link libpython, and apple's two-level
  namespace fails the link on undefined python symbols (android keeps
  its explicit libpython + 16KB flags).
- -DFLATBUFFERS_BUILD_FLATC=OFF / -DFLATBUFFERS_INSTALL=OFF on iOS: the
  FetchContent'd flatbuffers installs a TARGET flatc, which iOS's forced
  MACOSX_BUNDLE mode rejects ("no BUNDLE DESTINATION") - and codegen
  uses the cached HOST flatc anyway.
- The SHARED wrapper builds as .dylib on apple; the shim stages it under
  the .so extension name (a dylib is loadable as a python ext - the
  lightgbm precedent).

XNNPACK cross-compiles to iOS cleanly (default ON, all slices). All 3
iOS slices green; on-simulator 4/4 incl. REAL XNNPACK inference matching
desktop expectations to 1e-6. Tests: version-assert test removed per the
repo test conventions. meta reorganized by user; android lane unchanged
and re-verified by content (NDK marker + host entries per lane).
Flet 0.86's serious_python (4.3.0) only relocates ABI-tagged
(cpython-*/abi3) extension modules out of sitepackages.zip into an
importable jniLibs/.soref slot on Android; a plain '<name>.so' is
misfiled as a dependency lib and fails on-device with
'cannot import name _pywrap_tensorflow_interpreter_wrapper' (build
green, iOS fine). Stage the wrapper as
_pywrap_tensorflow_interpreter_wrapper<EXT_SUFFIX> (e.g.
.cpython-312.so) instead of a plain .so -- a valid CPython extension
suffix, so the import still resolves. Verified: the rebuilt arm64 wheel
now ships _pywrap_tensorflow_interpreter_wrapper.cpython-312.so.
Flet 0.86's serious_python (4.3.0) only relocates ABI-tagged
(cpython-*/abi3) extension modules out of sitepackages.zip into an
importable jniLibs/.soref slot on Android; a plain '<name>.so' is
misfiled as a dependency lib and fails on-device with
'cannot import name onnxruntime_pybind11_state' (build green, iOS fine).

Two changes: (1) the PEP 517 shim retags the built
onnxruntime_pybind11_state.so to the target EXT_SUFFIX (e.g.
.cpython-312.so) after staging; (2) setup.py builds package_data by
globbing the hardcoded plain 'libs' names, so the retagged file would be
dropped -- wildcard the pybind name and package the real basename.

Verified: forge-rebuilt arm64 wheel ships
onnxruntime/capi/onnxruntime_pybind11_state.cpython-312.so (present, 22MB).
serious_python 4.3.0 only relocates ABI-tagged (cpython-*/abi3) extension
modules into an importable jniLibs/.soref slot on Android; a plain
'<name>.so' bundled inside a package is misfiled as a dependency lib and
fails on-device with 'cannot import name'. Add a forge-error-catalogue
entry (error -> cause -> EXT_SUFFIX fix, incl. the setup.py-glob gotcha)
and a new-mobile-recipe PEP 517-shim bullet (always stage extensions with
EXT_SUFFIX). Precedents: machine/tflite-runtime, machine/onnxruntime-ext-suffix.
The EXT_SUFFIX retag + onnxruntime's copytree(dirs_exist_ok, which never
deletes) + forge reusing the source tree across slices meant a prior
slice's tagged .so lingered in onnxruntime/capi/. On iOS the per-slice
suffixes differ (.cpython-312-iphoneos.so vs -iphonesimulator.so), so the
wheel shipped BOTH and serious_python's .so->framework conversion failed
on-device (dlopen: onnxruntime.capi.onnxruntime_pybind11_state.framework
not found). Delete every onnxruntime_pybind11_state*.so that isn't the
current slice's target after retagging. (Android is unaffected -- all its
ABIs share .cpython-312.so; tflite is unaffected -- its shim rmtrees the
package dir each hook.)
A recipe with excluded_arches (onnxruntime drops armeabi-v7a) has no wheel
for the excluded ABI, but the recipe-tester's 'flet build apk' defaults to
all 3 Android ABIs -> pip 'No matching distribution for <pkg>' on the
excluded ABI, breaking the app build. stage_recipe.sh now derives
[tool.flet.android] target_arch from the wheels actually present in
dist-test/dist and writes it into the generated pyproject; recipes with
all ABIs (or pure-Python, no per-ABI wheel) stay unrestricted.
- new-mobile-recipe PEP517 bullet: forge reuses the source tree across
  slices, so a shim that overlays (not wipes) must delete stale per-slice
  .so -- iOS suffixes differ (iphoneos vs iphonesimulator) and both would
  ship, breaking framework conversion.
- forge-error-catalogue: 'No matching distribution for <pkg>' at APK build
  when a recipe has excluded_arches; stage_recipe.sh now auto-derives
  target_arch, and the mandatory target_arch note for Consumer notes.
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.

1 participant