LiquidAI/LFM2.5-Encoder-350M-Prompt-Router converted to LiteRT (.tflite) for on-device inference. Zero-shot prompt routing: define your routing lanes as free text and the model scores the whole prompt against every lane in one CPU pass (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))
Two signatures, route_128 and route_512 (S = 128 / 512, batch 1, right-padded, up to 8 lane slots):
1#!/usr/bin/env python32"""Route a prompt to one of your lanes with litert-community/LFM2.5-Encoder-350M-Prompt-Router."""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-Prompt-Router"11MAX_LANES =8121314defbuild_inputs(text, lanes, tokenizer, seq_len):15"""Builds input_ids/attention_mask plus the two mean-pool matrices."""16 body ="\n".join(f"- {lane}"for lane in lanes)17 prefix =f"Categories:\n{body}\n\nText:\n"18 encoding = tokenizer.encode(prefix + text)19 ids, offsets = encoding.ids, encoding.offsets
20iflen(ids)> seq_len:21raise SystemExit(f"{len(ids)} tokens exceed --seq-len {seq_len}")2223 input_ids = np.zeros((1, seq_len), np.int32)24 attention_mask = np.zeros((1, seq_len), np.int32)25 input_ids[0,:len(ids)]= ids
26 attention_mask[0,:len(ids)]=12728# Mean-pool over the document's own tokens.29 text_pool = np.zeros((1,1, seq_len), np.float32)30 text_idx =[i for i,(a, b)inenumerate(offsets)if b >len(prefix)and a != b]31 text_pool[0,0, text_idx]=1/len(text_idx)3233# Mean-pool over each lane's tokens; unused lane rows stay all-zero.34 category_pool = np.zeros((1, MAX_LANES, seq_len), np.float32)35 pos =len("Categories:\n")36for r, lane inenumerate(lanes):37 start, end = pos +2, pos +2+len(lane)38 pos = end +139 idx =[i for i,(a, b)inenumerate(offsets)if a < end and b > start and a != b]40 category_pool[0, r, idx]=1/len(idx)4142return{43"input_ids": input_ids,44"attention_mask": attention_mask,45"text_pool": text_pool,46"category_pool": category_pool,47}484950defmain():51 parser = argparse.ArgumentParser()52 parser.add_argument("--text", required=True,help="The prompt to route.")53 parser.add_argument("--lane", action="append", required=True,54help="A routing lane, repeatable (up to 8).")55 parser.add_argument("--seq-len",type=int, default=512, choices=[128,512])56 args = parser.parse_args()57iflen(args.lane)> MAX_LANES:58raise SystemExit(f"at most {MAX_LANES} lanes")5960 model_path = hf_hub_download(REPO,"LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite")61 tokenizer = Tokenizer.from_file(hf_hub_download(REPO,"tokenizer.json"))6263 feed = build_inputs(args.text, args.lane, tokenizer, args.seq_len)64 interpreter = Interpreter(model_path=model_path)65 runner = interpreter.get_signature_runner(f"route_{args.seq_len}")66 logits =list(runner(**feed).values())[0][0]6768# Softmax over the real lanes only — unused rows carry a constant bias logit.69 real = logits[:len(args.lane)]70 probs = np.exp(real - real.max())71 probs /= probs.sum()72for lane, p insorted(zip(args.lane, probs), key=lambda x:-x[1]):73print(f"{p:6.3f}{lane}")747576if __name__ =="__main__":77 main()
3. Run it
bash
1python route_prompt.py \2 --text "My Python script throws a KeyError on a dict lookup, how do I fix it?"\3 --lane "coding question" --lane "travel planning"\4 --lane "medical advice" --lane "small talk"
0.838 coding question
0.054 small talk
0.054 travel planning
0.054 medical advice
On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature names; the tokenizer is the standard Hugging Face tokenizer.json.
Performance
One pass over a padded sequence with the int8 (wi8fc) file, CPU only.
Device
Threads
route_128
route_512
Apple M4 Max (macOS)
8
34.5 ms
112.3 ms
iPhone 17 Pro
6
not measured
145 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, 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 372 ms against a 34.5 ms steady state. Later signatures on the same loaded model do not pay it again — route_512 measured 110 ms cold against 112 ms warm. Model load itself was 0.38 s on the iPhone, with a peak footprint of 649 MiB.
One pass scores the prompt against all eight lane slots at once, so the cost does not grow with the number of lanes. The signatures are fixed-shape, so input language or content does not change the time.
Accuracy note
Task-level parity against the PyTorch reference on the demo prompt with four lanes: fp32, fp16 and int8 all reproduce the reference lane probabilities to four decimal places — 0.838 for "coding question". That is a single-prompt spot check, not a benchmark over a labelled corpus.
On the iPhone 17 Pro the int8 file reproduces the desktop outputs bit-exactly — 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)
route_128
348 ms
133 ms
route_512
1558 ms
611 ms
GPU works as of the 2026-08-13 re-export. 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); the OpenCL delegate now takes the whole graph. Measured with the LiteRT CompiledModel API (fp32 GPU precision, real inputs incl. pooling matrices, best of 3 warm runs): route_51218.7 ms on the Pixel 8a, cosine 0.9949 vs the fp32 desktop reference; iPhone 17 Pro Metal route_512 172 ms, cosine 1.000000. Set the GPU precision to fp32 — at fp16 GPU precision this family's norm reductions overflow and every output is NaN. These CompiledModel timings are not comparable to the classic-delegate benchmark_model timings above (different GPU runtime).
Snapdragon NPU (Hexagon)
LFM2.5-Encoder-350M-Prompt-Router_fp16.tflite — the NPU runs it at 176.1 ms. The GPU does not — LiteRtException: Failed to compile model.
LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite — the GPU runs it at 80.59 ms. The NPU does not — LiteRtException: Failed to compile model.
file
backend
compiled
inference (median / min)
load
LFM2.5-Encoder-350M-Prompt-Router_fp16.tflite
NPU (Hexagon v81)
AOT (SM8850)
176.1 ms / 171.8 ms
434 ms
LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite
GPU (Adreno)
—
80.59 ms / 79.69 ms
7676 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. Every run held thermal status NONE throughout. Headroom 0.75–0.78, 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-Prompt-Router with modification notices per Section 4; all credit for the model to Liquid AI.