Skip to content

Add TFLite INT8 export - #813

Draft
EHxuban11 wants to merge 1 commit into
devfrom
feat/tflite-int8-export
Draft

EHxuban11 wants to merge 1 commit into
devfrom
feat/tflite-int8-export

Conversation

@EHxuban11

@EHxuban11 EHxuban11 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Requested by a user on LinkedIn: YOLOX trained in LibreYOLO, export --format tflite --int8 returned "TFLite INT8 quantization is not supported yet". A GitHub feature request is coming from them separately.

What changed

  • TFLiteExporter.supports_int8 = True; the hard rejection in _validate is gone.
  • export_tflite() takes int8 and calibration_data and appends -oiqt -cind <input> <calib.npy> 0.0 1.0 to the onnx2tf command.
  • Calibration batches come from the existing CalibrationDataLoader, written out as one NHWC float32 .npy through a memmap. Batch padding is trimmed so no image is weighted twice.
  • mean 0 / std 1 is deliberate: onnx2tf applies (value - mean) / std, and those batches already went through the model's own preprocessing. Any other value calibrates the wrong ranges.
  • INT8 uses the tf_converter backend. flatbuffer_direct refuses strict full-integer quantization on any op it cannot keep in int8 end to end and YOLO9 hits that on EQUAL. FP32 export still uses flatbuffer_direct, unchanged.
  • Artifact returned is *_full_integer_quant.tflite, falling back to *_integer_quant.tflite. It never falls back to a float artifact: that would hand back FP32 bytes under int8 metadata.
  • data= is mandatory (default_int8_calibration_data = False, same as TensorRT and OpenVINO). An eight-image default would produce a quietly wrong full-integer graph.
  • RF-DETR raises NotImplementedError for int8. That family converts through the Python API with a bespoke GridSample fixup that this path was not run against. Its FP32 tflite entry is blocked in the support matrix anyway.

No CLI change needed, --int8 --data already plumbed through.

Coverage

INT8 is gated on the existing per-family FP32 tflite support entry, so INT8 coverage equals FP32 coverage minus rfdetr. Today that means 2 of the 13 G0/G1 families:

group family tflite FP32 INT8 after this PR blocker
G0 yolo9 validated yes
G0 rfdetr blocked no LiteRT cannot allocate STRIDED_SLICE at 384x384
G1 yolonas validated yes
G1 yolo9_e2e blocked no public top-k class membership changes after conversion
G1 yolo9_p2 blocked no same
G1 ec blocked no ONNX_LAYERNORMALIZATION not preparable by LiteRT
G1 rtdetr blocked no CONCATENATION receives incompatible 256 and 1 dimensions
G1 dfine blocked no flatbuffer_direct GatherElements axis IndexError
G1 rtdetrv4 blocked no same GatherElements crash
G1 rtdetrv2 blocked no never validated through the ONNX-to-TFLite path
G1 deim blocked no never validated
G1 deimv2 blocked no never validated
G1 tinyformer blocked no never validated

Outside G0/G1, INT8 also becomes callable on every other family whose tflite entry is already validated: yolox, convnext, efficientnetv2, mobilenetv4, resnet, siglip2 (classify and embed), dinov2 (embed), pidnet, dexined, teed, realesrgan, swinir. YOLOX is the family the original request came from.

What we want: TFLite and TFLite INT8 for every G0 and G1 model

The target is not a wishlist. Every G0 and G1 family should have both FP32 TFLite and TFLite INT8, and the only acceptable reason for a family to be missing is that it is genuinely impossible to lower, not that nobody tried. RF-DETR is explicitly in scope: it is a flagship and it should run on TFLite.

Worth stating plainly, because it is the assumption that would otherwise excuse the DETR line: deformable attention is not what is blocking these families. RF-DETR's GridSample already has a working TFLite rewrite in libreyolo/export/tflite.py, and its FP32 block is a LiteRT allocation failure on STRIDED_SLICE at the 384x384 canvas. The D-FINE line is blocked on GatherElements and CONCATENATION. Those are converter and allocator defects, not an inherent limit of the architecture.

Ordered by expected difficulty:

  1. rfdetr (G0 flagship, mandatory). Fix the FP32 STRIDED_SLICE allocation failure, then teach the Python-API path to quantize. The GridSample work is already done.
  2. rtdetrv2, deim, deimv2, tinyformer. Marked "never validated", not "proven broken". Nobody has run them. Possibly free.
  3. dfine, rtdetrv4. The recorded crash is flatbuffer_direct specific. This PR's INT8 path already runs on tf_converter, so these entries may be testing the wrong backend.
  4. yolo9_e2e, yolo9_p2. Real numeric divergence in top-k membership; needs a fix, not a backend swap.
  5. ec. Needs LiteRT to gain ONNX_LAYERNORMALIZATION, or the op decomposed before conversion.

11 of the 13 have no working TFLite path at any precision, so this is FP32 unblocking work first and INT8 second. It is a separate body of work from this PR, not something this PR narrowed. If a family turns out to be truly unlowerable, that should be recorded as a specific converter or runtime defect with evidence, not left as an unexplained blocked entry.

A probe is running now over the blocked families: FP32 ONNX, converted with both onnx2tf backends on 2.6.8, then checked for LiteRT allocate and invoke. Results will be posted here and will say which of tiers 2 and 3 are actually reachable.

Verified

Real run, YOLO9-t at 640, onnx2tf 2.6.8 + LiteRT 2.1.2, random-init weights on synthetic images:

  • FP32 8.41 MB -> INT8 2.46 MB (3.42x)
  • selected _full_integer_quant.tflite, sidecar precision: int8
  • input tensor is int8, scale 0.00354 / zero -128
  • LibreYOLO(artifact) loads and runs it with no backend change: TFLiteBackend._quantize / _dequantize already handled quantized IO
  • raw output shape identical (1, 84, 8400); top-1 class agreement across the 8400 anchors 99.99%; normalized max abs diff 0.077

26 unit tests in tests/unit/test_export_tflite.py pass, plus test_export.py and test_export_support.py (229 passed, 3 skipped). Full CI green.

Not verified

  • No accuracy validation. Random weights and synthetic images say the plumbing is right, not that mAP holds. No family is parity-validated at int8, and the code logs a warning saying so. A real trained-checkpoint val() sweep is the follow-up before any support-matrix claim.
  • Only YOLO9 was converted. Every other tflite-validated family is untested at int8, YOLOX included.
  • Support matrix untouched: it has no precision axis, so INT8 rides on the existing per-family FP32 tflite entry. Deciding whether that axis should exist is a maintainer call.
  • No docs page. Export arguments live on the website, not in /docs.

Reviewer notes

  • The mean/std pair and the tf_converter backend switch are the two things worth a close look; both are load-bearing and both are covered by tests.
  • onnx2tf shells out to a bare onnxsim. Without it on PATH the INT8 backend fails with an unrelated-looking Keras error, so the code warns up front and adds a hint to the failure message. Worth deciding whether that should instead be a hard preflight check.

Code provenance

All code in this PR was written for this PR. No third-party code was copied, adapted, or derived. The change drives the existing onnx2tf dependency (Apache-2.0, already declared in the tflite extra) through its documented -oiqt / -cind command-line interface; no onnx2tf source was vendored or reimplemented.


Opened by an agent. It ran the tests and the conversion above; it did not review or approve the change.

export(format="tflite", int8=True, data=...) now runs onnx2tf post-training
quantization instead of rejecting the request, and returns the fully integer
artifact. Calibration batches are the model's own preprocessed tensors, so
onnx2tf is told not to normalize them again.

The integer path runs on the tf_converter backend: flatbuffer_direct aborts on
any op it cannot keep in int8 end to end, which YOLO9 hits on EQUAL.
FP32 export keeps flatbuffer_direct unchanged.
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