Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

- `BestModelCallback` no longer treats PyTorch Lightning's pre-training sanity-check validation pass as a real epoch's result. Its EMA-checkpoint tracking and the `smooth_alpha` smoothing accumulator are custom bookkeeping that sit outside `ModelCheckpoint`'s own `trainer.sanity_checking` guard (the guard the regular-checkpoint path already inherits), so a positive sanity-check score — common when starting a new training run initialized with `pretrain_weights` from a checkpoint pretrained on a different dataset — could get written out as the permanent "best" `checkpoint_best_ema.pth` before a single real epoch ran, and real training could then never surpass it. Note this is distinct from PTL's own `resume`/`ckpt_path` restart, which PTL itself skips the sanity check for (`not val_loop.restarting`). ([#1348](https://github.com/roboflow/rf-detr/issues/1348))

- TFLite INT8 documentation and warnings now reflect that dynamic-range quantization needs no calibration data. ([#1363](https://github.com/roboflow/rf-detr/issues/1363))

## [1.9.3] — 2026-08-17

### Added
Expand Down
90 changes: 9 additions & 81 deletions docs/learn/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: Export RF-DETR models to ONNX, TensorRT, TFLite, ExecuTorch, and na
- Export to ONNX for cross-platform inference with ONNX Runtime, OpenVINO, or TensorRT
- Export to TFLite (FP32, FP16, INT8) for mobile and edge deployment
- TensorRT conversion delivers lowest latency on NVIDIA GPUs (2.3 ms for Nano)
- INT8 quantization requires calibration data from your dataset for accurate results
- INT8 quantization is dynamic-range and needs no calibration data
- Custom input resolutions supported (must be divisible by `patch_size × num_windows`, which varies by model variant)
- Export to ExecuTorch for on-device PyTorch inference (XNNPACK, CoreML, QNN)
- Export directly to native CoreML (`.mlpackage`) for Xcode / Apple-platform deployment — see [Native CoreML Export](#native-coreml-export-mlpackage)
Expand Down Expand Up @@ -69,8 +69,8 @@ The `export()` method accepts several parameters to customize the export process
| `output_dir` | `"output"` | Directory where the exported model will be saved. |
| `format` | `"onnx"` | Export format: `"onnx"`, `"tflite"`, `"tensorrt"` (alias: `"trt"`), `"executorch"`, or `"coreml"`. |
| `quantization` | `None` | TFLite quantization mode: `None`/`"fp32"`, `"fp16"`, or `"int8"`. Only used when `format="tflite"`. |
| `calibration_data` | `None` | Calibration data for TFLite export. Image directory, `.npy` file path, NumPy array, or `None`. See [TFLite Export](#tflite-export). |
| `max_images` | `100` | Maximum number of images to load from a calibration directory for TFLite INT8 quantization. Ignored for other calibration data formats. |
| `calibration_data` | `None` | Optional image directory, `.npy` file path, NumPy array, or `None`. Not consumed when building the generated `.tflite` models. |
| `max_images` | `100` | Maximum number of images to load from a `calibration_data` directory. Ignored for other calibration data formats. |
| `infer_dir` | `None` | Optional directory of sample images for inference validation during export tracing. If not provided, a random dummy image is generated. |
| `backbone_only` | `False` | Export only the backbone feature extractor instead of the full model. |
| `opset_version` | `17` | ONNX opset version to use for export. Higher versions support more operations. |
Expand Down Expand Up @@ -255,7 +255,7 @@ predictions = model(image)

- `onnx2tf` output graph structure can change between minor versions, silently altering output tensor layout and breaking downstream inference code.
- `ai_edge_litert` (Google's replacement for `tflite-runtime`) is still stabilising its public API; version pinning is strongly recommended.
- INT8 quantization accuracy is sensitive to calibration data quality — poor calibration causes silent precision loss with no error at export time.
- INT8 quantization is dynamic-range (INT8 weights, float activations). It is applied without calibration, and quantizing a transformer's weights to 8 bits can still cost accuracy — validate the INT8 model before deploying it.
- The ONNX → TF → TFLite conversion chain introduces numerical rounding that may produce slightly different predictions from the original PyTorch model.
- Installation of the `[tflite]` extra may conflict with existing TensorFlow or NumPy versions in your environment.
- `onnx` and TensorFlow both bundle Abseil and export its symbols weakly, so whichever loads first supplies them to both. RF-DETR imports TensorFlow first on the TFLite route; if your own code imports `onnx` before `tensorflow`, RF-DETR logs a warning and the conversion may block forever while restoring the SavedModel (no error, 0% CPU). Importing `onnx` *after* `tensorflow` is safe; otherwise, in a fresh process, preload/import `tensorflow` before `onnx` and then run the export — freshness alone is not sufficient.
Expand Down Expand Up @@ -299,92 +299,20 @@ pip install "rfdetr[tflite]"

This produces both `output/inference_model_fp32.tflite` and `output/inference_model_fp16.tflite`.

### INT8 Quantization with Calibration Data
### INT8 Quantization

For INT8 quantization, provide representative images from your dataset as calibration data. This is **critical** for preserving model accuracy — without real calibration data, the quantizer uses random noise and accuracy will be poor.
`quantization="int8"` produces a **dynamic-range** INT8 model: weights are stored as INT8, activations stay in float, and the weight scales are derived from the weights themselves. No calibration data is required, and supplying it does not change the result — static/full-integer INT8, the mode that *would* need representative data, is intentionally unsupported because RF-DETR's transformer activations do not survive it.

#### Option 1: Point to an Image Directory (Recommended)

The simplest approach — just point `calibration_data` to a directory containing JPEG/PNG images. The converter automatically loads, resizes, and prepares the images:

```python
from rfdetr import RFDETRNano

model = RFDETRNano()
model.export(
format="tflite",
quantization="int8",
calibration_data="path/to/val2017/", # directory of images
output_dir="output",
)
```

The converter loads up to 100 images from the directory by default, resizes them to the model's input resolution, and uses them for both output validation and INT8 calibration. Supported formats: JPEG, PNG, BMP, WebP.

You can control how many images are loaded with the `max_images` parameter:
`calibration_data` accepts a directory of JPEG, PNG, BMP or WebP images, a path to an `.npy` file of shape `(N, H, W, 3)` (float32, values in `[0, 1]`), or a NumPy array in that format; `max_images` caps how many images are read from a directory. These arguments are not consumed when building the generated `.tflite` models. Omitting them is the normal path:

```python
model.export(
format="tflite",
quantization="int8",
calibration_data="path/to/val2017/",
max_images=200, # load up to 200 images (default: 100)
output_dir="output",
)
```

#### Option 2: NumPy `.npy` File

Prepare calibration data as a NumPy array and save it to a `.npy` file:

- Shape: `(N, H, W, 3)` — NHWC format with 3 color channels
- Data type: `float32`
- Value range: `[0, 1]` (divide by 255, but do **not** apply ImageNet normalization — the converter handles that automatically)
- Recommended: 20–100 representative images from your dataset

```python
import numpy as np
from PIL import Image
import torchvision.transforms.functional as F
from rfdetr import RFDETRSmall

model = RFDETRSmall()
target_resolution = model.model_config.resolution

# Load representative images from your dataset
images = []
for path in image_paths[:50]: # 50 representative samples
img = Image.open(path).convert("RGB")
image_tensor = F.to_tensor(img)
image_tensor = F.resize(image_tensor, [target_resolution, target_resolution], antialias=False)
images.append(image_tensor.permute(1, 2, 0).contiguous().numpy())

calibration_data = np.stack(images) # shape: (50, H, W, 3)

# Save to .npy for reuse
np.save("calibration_data.npy", calibration_data)

# Export with INT8 quantization
model.export(
format="tflite",
quantization="int8",
calibration_data="calibration_data.npy",
output_dir="output",
)
model.export(format="tflite", quantization="int8", output_dir="output")
```

#### Option 3: NumPy Array Directly

You can also pass the NumPy array directly without saving to disk:

```python
model.export(
format="tflite",
quantization="int8",
calibration_data=calibration_data, # np.ndarray
output_dir="output",
)
```
This writes `output/inference_model_dynamic_range_quant.tflite` alongside the FP32 and FP16 models.

### FP16 Export

Expand Down
21 changes: 11 additions & 10 deletions src/rfdetr/detr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1555,21 +1555,22 @@ def export(
instabilities (``onnx2tf``, ``ai_edge_litert``, ``executorch``, ``coremltools``) may affect results.
quantization: TFLite quantization mode (ignored when
``format="onnx"``). One of ``None``, ``"fp32"``, ``"fp16"``, ``"int8"``. ``None`` / ``"fp32"`` /
``"fp16"`` produce FP32 + FP16 ``.tflite`` files; ``"int8"`` additionally produces an INT8-quantized
model.
calibration_data: Representative images for INT8 calibration and ``onnx2tf`` output validation. Accepts:
``"fp16"`` produce FP32 + FP16 ``.tflite`` files; ``"int8"`` additionally produces a dynamic-range
INT8 model (INT8 weights, float activations; needs no calibration data).
calibration_data: Optional data not consumed when building the exported ``.tflite`` models. Accepts:

* ``None`` — auto-generate random data (sufficient for fp32/fp16; warns for int8).
* ``None`` — auto-generate random data (the default, and adequate for every quantization mode).
* A **directory path** (``str``) containing JPEG/PNG
images — the converter automatically loads, resizes, and prepares them. This is the simplest
approach.
images — the converter automatically loads, resizes, and prepares them.
* A path (``str``) to a ``.npy`` file of shape ``(N, H, W, 3)``, dtype float32, values in ``[0, 1]``.
* A :class:`numpy.ndarray` with the same format.

For INT8 quantization, provide 20–100 representative images from your training/validation set for best
accuracy.
max_images: Maximum number of images to load from a calibration directory. Defaults to ``100``. Only used
when *calibration_data* is a directory path.
This does **not** improve INT8 accuracy: ``quantization="int8"`` produces a dynamic-range model whose
weight scales come from the weights themselves. When passed as ``None``, a directory, or an array, the
data is saved to ``_rfdetr_calib_data.npy`` in *output_dir* but not consumed to build the model. An
existing ``.npy`` path is reused without writing a copy.
max_images: Maximum number of images to load from a *calibration_data* directory. Defaults to ``100``.
Only used when *calibration_data* is a directory path.
backend: Hardware backend to specialize the export for. Required when ``format="executorch"`` and
ignored — with a warning — for any other format. Accepted values for ExecuTorch:
``"xnnpack"`` (portable CPU, fp32), ``"coreml"`` (Apple devices, fp16; requires ``coremltools``),
Expand Down
Loading
Loading