LiquidAI/LFM2.5-Encoder-350M-Spellchecker converted to LiteRT (.tflite) for on-device inference. A GECToR-style two-head tagger that corrects misspellings and grammar token by token, fully offline (demo Space).
desktop CPU — phone CPU memory limits (XNNPACK per-signature fp32 unpacking); this is the file the Snapdragon NPU runs, AOT-compiled (see Snapdragon NPU (Hexagon))
One signature, gec_128 (S = 128, batch 1, right-padded; the base model's own decode also uses max_len 128). input_ids int32 [1, 128] — the tokenizer prepends <|startoftext|>, which the model uses as the sentence anchor — and attention_mask int32 [1, 128]. Two outputs, both zeroed at padded positions:
Output
Shape
Meaning
output_0 (label_logits)
float32 [1, 128, 128802]
per-token edit tag
output_1 (detect_logits)
float32 [1, 128, 2]
P(token is part of an error) gate
The tag space is 0 = $KEEP, 1 = $DELETE, 2 … 2+V = $REPLACE_<piece>, 2+V … = $APPEND_<piece>, with V = 64400 BPE pieces. For a $REPLACE/$APPEND tag the piece id is the tag minus its base — i.e. tag 7393 means "replace with vocabulary id 7391", which is Ġgoes.
Decoding is the base repo's algorithm: argmax the tags, gate them by softmax(detect)[1] >= min_error_prob, apply the surviving edits, and repeat (at most 3 passes) until the text stops changing. The base repo also bundles an optional PyTorch reranker for its published maximum-precision operating point; that stays host-side on desktop. This artifact covers the tagger, which is a fully supported mode of the base model's .correct().
1#!/usr/bin/env python32"""Correct text with litert-community/LFM2.5-Encoder-350M-Spellchecker."""3import argparse
45import numpy as np
6from ai_edge_litert.interpreter import Interpreter
7from huggingface_hub import hf_hub_download
8from tokenizers import Tokenizer
910REPO ="litert-community/LFM2.5-Encoder-350M-Spellchecker"11SEQ_LEN =12812VOCAB =64400# $REPLACE_<piece> occupies tags 2..2+VOCAB, $APPEND_<piece> the rest131415defcorrect_once(ids, runner, min_error_prob):16"""One tagging pass. Returns the edited id list and whether anything changed."""17 input_ids = np.zeros((1, SEQ_LEN), np.int32)18 attention_mask = np.zeros((1, SEQ_LEN), np.int32)19 input_ids[0,:len(ids)]= ids
20 attention_mask[0,:len(ids)]=12122 out = runner(input_ids=input_ids, attention_mask=attention_mask)23 label_logits, detect_logits = out["output_0"][0], out["output_1"][0]2425 edits =[]26for t inrange(len(ids)):27 scores = detect_logits[t]28 error_prob = np.exp(scores[1]- scores.max())/ np.exp(scores - scores.max()).sum()29if error_prob < min_error_prob:30continue31 tag =int(label_logits[t].argmax())32if tag ==0:# $KEEP33continue34 edits.append((t, tag))3536 edited =list(ids)37for t, tag inreversed(edits):# right-to-left keeps earlier indices valid38if tag ==1:# $DELETE39del edited[t]40elif tag <2+ VOCAB:# $REPLACE_<piece>41 edited[t]= tag -242else:# $APPEND_<piece>43 edited.insert(t +1, tag -2- VOCAB)44return edited,bool(edits)454647defmain():48 parser = argparse.ArgumentParser()49 parser.add_argument("--text", required=True,help="Text to correct.")50 parser.add_argument("--min-error-prob",type=float, default=0.5)51 parser.add_argument("--max-passes",type=int, default=3)52 args = parser.parse_args()5354 model_path = hf_hub_download(REPO,"LFM2.5-Encoder-350M-Spellchecker_wi8fc.tflite")55 tokenizer = Tokenizer.from_file(hf_hub_download(REPO,"tokenizer.json"))5657 ids = tokenizer.encode(args.text).ids # the tokenizer prepends the BOS anchor58iflen(ids)> SEQ_LEN:59raise SystemExit(f"{len(ids)} tokens exceed the {SEQ_LEN}-token window")6061 interpreter = Interpreter(model_path=model_path)62 runner = interpreter.get_signature_runner("gec_128")6364for _ inrange(args.max_passes):65 ids, changed = correct_once(ids, runner, args.min_error_prob)66ifnot changed:67break6869print(tokenizer.decode(ids).strip())707172if __name__ =="__main__":73 main()
3. Run it
python spellcheck.py --text "I has recieved you're mesage yesterday and will responde soon."
I have received your message yesterday and will respond soon.
A sentence with nothing to fix comes back unchanged. On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature name; the tokenizer is the standard Hugging Face tokenizer.json.
Performance
One gec_128 pass with the int8 (wi8fc) file, CPU only.
Device
Threads
gec_128
Apple M4 Max (macOS)
8
49.8 ms
iPhone 17 Pro
6
64 ms
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 per output head — both heads measured 64 ms — 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 299 ms against a 49.8 ms steady state. Model load itself was 0.28 s on the iPhone, with a peak footprint of 746 MiB.
Correction is iterative, so a three-pass correction runs the graph three times. The signature is fixed-shape, so input language or content does not change the per-pass time.
Accuracy note
Task-level parity against the PyTorch reference on "She go to school every day ." — a single $REPLACE on "go": fp32, fp16 and int8 all produce the identical edit, at the same position, with the same replacement piece and an agreeing detect head. That is a single-sentence spot check, not a benchmark over a labelled corpus.
On the iPhone 17 Pro the int8 file reproduces the desktop outputs bit-exactly on both heads — including the full [1, 128, 128802] label tensor — at cosine 1.000000, max absolute difference 0.0.
Android (Pixel 8a)
Android figures use the standard TFLite benchmark_model 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)
gec_128
467 ms
168 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 128802-token tied-vocabulary head hits a runtime kernel limit ("failed to initialize kernel"), consistently on Metal and OpenCL. CPU remains the mobile path for the int8 file (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). It is also the only file here that reached a mobile accelerator: AOT-compiled for the Hexagon it runs on a Galaxy S26 NPU at 82.96 ms (see Snapdragon NPU (Hexagon) below), while the int8 file produced no usable row on either S26 accelerator.
Snapdragon NPU (Hexagon)
LFM2.5-Encoder-350M-Spellchecker_fp16.tflite — the NPU runs it at 82.96 ms. The GPU does not — LiteRtException: Failed to compile model.
LFM2.5-Encoder-350M-Spellchecker_wi8fc.tflite — neither accelerator produced a usable row on the S26. NPU: the graph compiles and then fails to run (LiteRtException: Failed to invoke the compiled model). GPU: LiteRtException: Failed to compile model.
file
backend
compiled
inference (median / min)
load
LFM2.5-Encoder-350M-Spellchecker_fp16.tflite
NPU (Hexagon v81)
AOT (SM8850)
82.96 ms / 77.98 ms
323 ms
Measured on a Samsung Galaxy S26 (Snapdragon 8 Elite Gen 5 / SM8850, Hexagon v81, Android 16) with LiteRT CompiledModel 2.2.0, one accelerator per process, 5 warm-up runs then N=50 timed runs, median reported. The run held thermal status NONE throughout. Headroom 0.76–0.77, where 1.0 is the throttling threshold.
The NPU row marked AOT ran an artifact compiled ahead of time for SM8850 (ai-edge-litert 2.2.0 + QAIRT 2.47.0), not the published file. That artifact is not distributed here; the compile is one command in the NPU guide.
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-Spellchecker with modification notices per Section 4; all credit for the model to Liquid AI.