--- license: other license_name: lfm1.0 license_link: LICENSE base_model: LiquidAI/LFM2.5-Encoder-350M-PII-Detector pipeline_tag: token-classification library_name: litert tags: - litert - tflite - on-device - edge - encoder - pii - ner - privacy - liquid - lfm2 - lfm2.5 --- # LFM2.5-Encoder-350M-PII-Detector — LiteRT [LiquidAI/LFM2.5-Encoder-350M-PII-Detector](https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-PII-Detector) converted to **LiteRT** (`.tflite`) for on-device inference. Detects ~40 kinds of personal information across 16 languages, fully offline — a natural fit for on-device redaction where the text must never leave the phone ([demo Space](https://huggingface.co/spaces/LiquidAI/pii-detection)). ## Model description | File | Recipe | Size | Target | |---|---|---|---| | `LFM2.5-Encoder-350M-PII-Detector_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 364 MB | mobile + desktop | | `LFM2.5-Encoder-350M-PII-Detector_fp16.tflite` | fp16 weights, float compute | 712 MB | desktop — full fidelity; phone memory limits (XNNPACK per-signature fp32 unpacking) | Two signatures, `pii_128` and `pii_512` (S = 128 / 512, batch 1, right-padded): | Tensor | Shape | Meaning | |---|---|---| | `input_ids` | int32 `[1, S]` | token ids (the tokenizer prepends `<\|startoftext\|>`) | | `attention_mask` | int32 `[1, S]` | 1 = real token, 0 = pad | | `output_0` | float32 `[1, S, 161]` | BIOES logits, zeroed at padded positions | Take the argmax per token, then decode the BIOES spans. The logit axis is 161 wide but only ids **0–108** are defined — `label_schema.json` carries the 109-entry `id2label` map (id 0 = `O`, "not personal information"); the remaining slots are unused. Restricting the argmax to the first 109 columns is the safe reading. ## How to use **1. Install dependencies** ```bash pip install ai-edge-litert numpy tokenizers huggingface_hub ``` **2. Save the script** below as `detect_pii.py`: ```python #!/usr/bin/env python3 """Tag personal information with litert-community/LFM2.5-Encoder-350M-PII-Detector.""" import argparse import json import numpy as np from ai_edge_litert.interpreter import Interpreter from huggingface_hub import hf_hub_download from tokenizers import Tokenizer REPO = "litert-community/LFM2.5-Encoder-350M-PII-Detector" def main(): parser = argparse.ArgumentParser() parser.add_argument("--text", required=True, help="Text to scan.") parser.add_argument("--seq-len", type=int, default=128, choices=[128, 512]) args = parser.parse_args() model_path = hf_hub_download(REPO, "LFM2.5-Encoder-350M-PII-Detector_wi8fc.tflite") tokenizer = Tokenizer.from_file(hf_hub_download(REPO, "tokenizer.json")) schema = json.load(open(hf_hub_download(REPO, "label_schema.json"))) id_to_label = {int(i): name for i, name in schema["id2label"].items()} encoding = tokenizer.encode(args.text) if len(encoding.ids) > args.seq_len: raise SystemExit(f"{len(encoding.ids)} tokens exceed --seq-len {args.seq_len}") input_ids = np.zeros((1, args.seq_len), np.int32) attention_mask = np.zeros((1, args.seq_len), np.int32) input_ids[0, : len(encoding.ids)] = encoding.ids attention_mask[0, : len(encoding.ids)] = 1 interpreter = Interpreter(model_path=model_path) runner = interpreter.get_signature_runner(f"pii_{args.seq_len}") logits = runner(input_ids=input_ids, attention_mask=attention_mask)["output_0"] # Only ids 0..108 are defined in label_schema.json; the rest are unused slots. tags = logits[0, : len(encoding.ids), : schema["num_labels"]].argmax(-1) for token, tag in zip(encoding.tokens, tags): if int(tag) != 0: # 0 = O, "not personal information" print(f"{token:20} {id_to_label[int(tag)]}") if __name__ == "__main__": main() ``` **3. Run it** ```bash python detect_pii.py --text "My email is jane@example.com and my phone number is 555-0142." ``` ``` Ġjane B-contact.email @example I-contact.email .com E-contact.email 555 B-contact.phone - I-contact.phone 014 I-contact.phone 2 E-contact.phone ``` On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature names; the tokenizer is the standard Hugging Face `tokenizer.json`, which the Rust/Swift/Kotlin `tokenizers` bindings all read. ## Performance One pass over a padded sequence with the int8 (`wi8fc`) file, CPU only. | Device | Threads | `pii_128` | `pii_512` | |---|---|---|---| | Apple M4 Max (macOS) | 8 | 34.9 ms | 111.9 ms | | iPhone 17 Pro | 6 | 52 ms | not measured | Mac figures are the median of 20 warm runs (`ai-edge-litert` 2.1.6, XNNPACK, otherwise idle machine). The iPhone figure comes from the on-device gate (TFLite C API + SignatureRunner + XNNPACK) and is a single run, not a median. **Budget for one slow first call.** The first inference after loading pays a one-time graph preparation: on the Mac it took 376 ms against a 34.9 ms steady state. Later signatures on the same loaded model do not pay it again — `pii_512` measured 110 ms cold against 112 ms warm. Model load itself was 0.38 s on the iPhone. The signatures are fixed-shape, so the input language or content does not change the time — warm, `pii_128` measures 36.4 / 37.3 / 36.6 ms on English, Japanese and Arabic sentences of 17, 21 and 27 tokens. ### Accuracy note Task-level parity against the PyTorch reference on a name + email + phone sentence: fp32 and fp16 reproduce the reference entity tags **exactly**. int8 keeps all multi-token spans (email, phone) intact and dropped exactly one tag in that test — an entity-end token whose fp32 decision margin was only 0.53 logits, a genuinely borderline call. That is the extent of what was checked; it is a single-sentence spot check, not a benchmark over a labelled corpus. If you need maximum recall on borderline tokens, use the fp16 file on desktop; on phones the int8 file is the artifact. On the iPhone 17 Pro the int8 file reproduces the desktop outputs **bit-exactly** — cosine 1.000000, max absolute difference 0.0 over the full output tensor. ### Android (Pixel 8a) Android figures use the standard TFLite [`benchmark_model`](https://ai.google.dev/edge/litert/models/measurement) on a **Pixel 8a** (Tensor G3, Android 16) — 5 warm-up runs then 20 timed runs, the signature selected explicitly with `--signature_to_run_for`, CPU at 4 threads. | Signature | GPU (OpenCL, previous export) | CPU (XNNPACK, 4 threads) | |---|---|---| | `pii_128` | 342 ms | **110 ms** | | `pii_512` | 1656 ms | **603 ms** | **GPU status (2026-08-13 re-export): still CPU on mobile.** The re-export respells the one idiom mobile GPU delegates refuse — transformers' rank-5 `repeat_kv` expand — into an equivalent rank-4 matmul (outputs bitwise-identical on CPU) — removing the family-wide GPU blocker — but this model's int8 file still does not compile on mobile GPU delegates: its 161-label classification head hits a runtime kernel limit ("failed to initialize kernel"), consistently on Metal and OpenCL. **CPU remains the mobile path** (bit-exact on device). The fp16 file does run under the GPU delegates with the head falling back to CPU and matches the fp32 reference (cosine 1.000000, desktop-verified) — a desktop option, not a mobile one. ## License LFM Open License v1.0 (see `LICENSE`, unchanged from the base model). Note the license's commercial-use threshold (Section 5). This repository redistributes converted **Derivative Works** of LiquidAI/LFM2.5-Encoder-350M-PII-Detector with modification notices per Section 4; all credit for the model to [Liquid AI](https://www.liquid.ai/).