When an animal-identification app has to work in the field, “the model runs” is only the beginning. The image contract, export path, hardware delegate, memory lifecycle, and fallback behavior all affect the result users see.
Safari Go uses a two-stage cascade that runs locally on Android. This article walks through the engineering path from training data to an on-device result, including the hardware path we use today and the boundaries we do not claim to have solved yet.
1. The target: private, offline identification
The Android inference path has four practical requirements:
- The photo should be decoded and classified on the device. Network calls are reserved for product services such as trial state, entitlements, purchases, and guide downloads.
- PyTorch and Android must agree on the same input, output, and coordinate contracts.
- GPU acceleration should be used only after a real compatibility and delegate probe succeeds.
- A device that cannot run the delegate must still have a deterministic FP32 CPU path instead of a crash or a false “GPU accelerated” claim.
The result is a conventional edge-AI pipeline: detect the animal first, crop a region of interest, and classify that crop locally.
2. Overall architecture
The production stack contains two FP32 models:
| Stage | Android contract |
|---|---|
| Detector | RT-DETR-R18, 512×512 RGB, batch 1 |
| Detector output | NMS-free raw [100, 9] rows for animal, person, vehicle |
| Detection threshold | Fixed 0.10 |
| ROI selection | Highest-score animal, with stable query-order tie breaking |
| ROI padding | 18% in source-image coordinates |
| Classifier | MobileNetV4-Conv-Medium, 320×320 RGB |
| Classifier output | 49 species logits, exposed as top-1 and top-3 |
| Low-confidence state | Classifier top-1 below 0.55 is shown as low confidence |
| Super-resolution | Disabled in the first-launch stack |
flowchart TD
A[Camera or photo] --> B[Canonical RGB image]
B --> C[512x512 RGB letterbox]
C --> D[RT-DETR-R18 FP32]
D --> E[Decode raw 100x9 rows]
E --> F[Choose highest-score animal]
F --> G[Inverse-map box]
G --> H[Add 18% padded ROI]
H --> I[320x320 RGB letterbox]
I --> J[MobileNetV4 FP32]
J --> K[Top-1 / top-3 result]
Both models share the same image rules: EXIF-normalized orientation, sRGB
8-bit RGB pixels, a bounded source long edge, aspect-fit letterboxing, and gray
128 padding. This is more important than it looks. A different rotation,
color conversion, or padding value changes the tensor before the model has a
chance to make a prediction.
3. Training and export pipeline
3.1 Data and training
Training starts with an auditable manifest. Included images must satisfy the project’s location, date, license, attribution, and normalized-taxon rules. The classifier label set is fixed at 49 species.
Training runs on a CUDA-enabled NVIDIA host. The production classifier is MobileNetV4-Conv-Medium trained with a PyTorch/timm wrapper. The interim detector is RT-DETR-R18. The Android resolver accepts the FP32 production classifier only; benchmark and research paths do not silently become release inputs.
3.2 Export and validation
The export path uses LiteRT-Torch to produce .tflite files. The export wrapper
keeps the forward definition and image normalization explicit, then runs the
following checks before an artifact can enter the Android production asset
directory:
- Validate input/output shape and dtype.
- Load, allocate, and execute the LiteRT interpreter.
- Reject custom and Flex operators.
- Compare the exported output with the PyTorch reference.
- Write a manifest containing artifact SHA-256, checkpoint, converter/runtime versions, preprocessing, and output schema.
flowchart TD
A[Licensed data manifests] --> B[PyTorch training]
B --> C[Checkpoint]
C --> D[LiteRT-Torch export]
D --> E[Interpreter smoke test]
E --> F[PyTorch/LiteRT parity]
F --> G[SHA + schema manifest]
G --> H[Production model assets]
H --> I[Android model resolver]
The current release identities are:
| Artifact | Role | SHA-256 |
|---|---|---|
knp_detector_rtdetr_r18_v1.tflite | RT-DETR-R18 FP32 detector | 3a43fdf973c03844f30ac088542da29ced0c565b828e0b203d74d1c096729d4a |
knp_m1_coreml_v1_0_fp32.tflite | MobileNetV4 FP32 production baseline | 6fa1f47fbb2b6e9f133daae03ef4e644114160dbddfc45916181ffbf7a2f3251 |
Parity and model quality are separate checks. The classifier’s recorded maximum
PyTorch/LiteRT error is 6.85e-06, within the 1e-4 conversion gate. The
detector export also has a passing conversion/parity result on the fixed
fixtures. A2 detector quality is still tracked separately and remains a
blocked research status; a passing export does not turn into a claim that
field recall is solved.
4. Android inference: the cascade in code
At runtime, the app performs the same sequence for a camera frame or a photo:
- Canonical RGB image.
512×512letterbox.- RT-DETR raw
[100, 9]output. - Decode boxes, scores, and labels.
- Choose the highest-score animal.
- Inverse-map the box and add an 18% padded crop.
320×320letterbox.- MobileNetV4 49-class logits.
- Top-1, top-3, and confidence state.
The detector stays NMS-free in the model. Android computes the application
score as score = objectness * max(animal_score, person_score, vehicle_score).
The selected animal box is mapped back to the original image before padding.
That keeps the classifier ROI independent of the detector’s 512×512 padded
coordinate space.
If no animal candidate passes the fixed detector threshold, the pipeline returns a clear no-animal result. If the classifier score is weak, the UI keeps the top candidates and labels the result as low confidence instead of treating an uncertain species as confirmed.
5. Hardware acceleration: GPU first, CPU fallback
The Android app uses the standalone LiteRT runtime and GPU delegate libraries
at the same version (1.4.2). Production requests GPU_FIRST, but the app
does not assume that a device GPU can execute this particular graph.
The runtime performs a capability check, creates a real delegate as a probe,
and then invokes the model. Only a successful invocation is recorded as GPU
execution. If the device is unsupported, delegate creation fails, or inference
throws, the same model runs through the FP32 CPU interpreter and records
CPU_FALLBACK.
flowchart TD
A[Input tensor] --> B{GPU compatibility check}
B -- No --> C[FP32 CPU interpreter]
B -- Yes --> D{Delegate probe}
D -- Failed --> C
D -- Passed --> E[GPU delegate + interpreter]
E --> F{Invocation succeeds?}
F -- No --> C
F -- Yes --> G[Record GPU_FP16]
C --> H[Record CPU_FALLBACK]
The model file remains FP32 on both paths. GPU_FP16 describes delegate
execution; it is not a separately trained FP16 or INT8 artifact. The GPU
delegate and interpreter are created and used on the same single-thread
executor, which is required by the delegate’s thread-affinity contract.
There is no universal Android NPU switch. NPU execution requires a vendor-specific LiteRT delegate and a model/operator combination that the delegate supports. We therefore do not label a CPU run as NPU acceleration, and we do not use NNAPI as a generic MediaTek solution. The current hardware path follows the LiteRT GPU delegate pattern; vendor-specific NPU options are described in the LiteRT NPU documentation.
6. Memory and stability on real phones
The current detector artifact is about 81.8 MB and the classifier artifact is about 34.1 MB on disk. Native interpreter graphs, delegate allocations, image buffers, and tensor storage add to that footprint. This is why APK size and runtime memory are separate measurements.
The production runner avoids keeping both native model graphs alive during one identification:
sequenceDiagram
participant P as Cascade pipeline
participant D as Detector runtime
participant C as Classifier runtime
P->>D: map model and run
D-->>P: raw detector rows
P->>D: close interpreter, delegate, and mapping
P->>C: map model and run ROI
C-->>P: 49 logits
P->>C: close interpreter, delegate, and mapping
P-->>P: release image and ROI buffers
The implementation also:
- memory-maps uncompressed model assets read-only;
- reuses direct input/output buffers on the inference executor;
- bounds decoded source images to a 2048-pixel long edge;
- starts CPU fallback at two interpreter threads;
- explicitly closes the interpreter, delegate, mapping, and executor;
- keeps SR out of the required model stack.
The benchmark harness runs warmups and measured repetitions for both CPU and GPU delegates, recording P50/P95 latency, thermal status, native RSS, and finite-output status. A Pixel 4 XL is useful as a stress case; it should not be treated as proof that every Android device has the same memory or acceleration profile.
7. Trace the full path when a result looks wrong
Debug builds can trace six stages:
INPUT -> DETECTOR_RAW -> BBOX -> ROI -> CLASSIFIER_RAW -> RESULT.
The trace contract records hashes and structured metadata rather than relying on a screenshot alone:
- source and canonical RGB hashes;
- detector input hash and raw shape;
- detector and classifier artifact hashes;
- selected detection and source-space ROI;
- classifier top-3;
- backend and latency;
- final result state.
Release builds do not write the original photo or complete logits to logs. They retain only aggregate backend and latency information. This lets us compare a Pixel, a MediaTek device, and a Samsung device without turning production logs into a copy of the user’s image.
8. What is supported today
The Android path is a real FP32 cascade with:
- a fixed detector/classifier contract;
- artifact SHA and schema validation;
- local inference from Camera or Photos;
- optional GPU execution based on an observed delegate probe;
- deterministic FP32 CPU fallback;
- explicit model lifecycle management for memory-constrained phones;
- debug-only stage tracing and release-safe aggregate telemetry.
The MobileNetV4 FP32 classifier is the production baseline. RT-DETR-R18 is the current detector artifact used by the first-launch stack, while its independent quality gate remains a separate improvement track. The next gains must come from better detector quality or a verified hardware delegate—not from silently changing the preprocessing contract or claiming unsupported NPU execution.
Conclusion
The practical lesson from this deployment is simple: mobile inference is a system, not a model file. Training, export, preprocessing, delegate selection, memory lifecycle, and observability must agree on the same contract.
For Safari Go, that contract currently means RT-DETR-R18 FP32 followed by MobileNetV4 FP32, GPU when it is proven usable, CPU when it is not, and a local photo pipeline that can be inspected stage by stage.