Instructions to use litert-community/LFM2.5-Encoder-350M-Prompt-Router with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use litert-community/LFM2.5-Encoder-350M-Prompt-Router with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Card: measured Performance table (M4 Max + iPhone 17 Pro), verified runnable usage script, accuracy note
Browse files
README.md
CHANGED
|
@@ -11,8 +11,8 @@ tags:
|
|
| 11 |
- on-device
|
| 12 |
- edge
|
| 13 |
- encoder
|
| 14 |
-
- zero-shot
|
| 15 |
- routing
|
|
|
|
| 16 |
- liquid
|
| 17 |
- lfm2
|
| 18 |
- lfm2.5
|
|
@@ -22,52 +22,153 @@ tags:
|
|
| 22 |
|
| 23 |
[LiquidAI/LFM2.5-Encoder-350M-Prompt-Router](https://huggingface.co/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](https://huggingface.co/spaces/LiquidAI/prompt-routing)).
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
|---|---|---|---|
|
| 27 |
-
| `LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 365 MB | mobile + desktop
|
| 28 |
| `LFM2.5-Encoder-350M-Prompt-Router_fp16.tflite` | fp16 weights, float compute | 713 MB | desktop — phone memory limits (XNNPACK per-signature fp32 unpacking) |
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
| Input | Shape | |
|
| 35 |
|---|---|---|
|
| 36 |
-
| `input_ids` | int32 `[1, S]` |
|
| 37 |
-
| `attention_mask` | int32 `[1, S]` | 1 = token, 0 = pad |
|
| 38 |
-
| `text_pool` | float32 `[1, 1, S]` | mean-pool weights over the prompt's
|
| 39 |
-
| `category_pool` | float32 `[1, 8, S]` | row r = mean-pool weights over lane r's tokens; unused
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
```python
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
import numpy as np
|
|
|
|
|
|
|
| 47 |
from tokenizers import Tokenizer
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
prefix = f"Categories:\n{body}\n\nText:\n"
|
| 52 |
-
|
| 53 |
-
ids,
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
pos = len("Categories:\n")
|
| 61 |
for r, lane in enumerate(lanes):
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
```
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
## License
|
| 73 |
|
|
|
|
| 11 |
- on-device
|
| 12 |
- edge
|
| 13 |
- encoder
|
|
|
|
| 14 |
- routing
|
| 15 |
+
- zero-shot
|
| 16 |
- liquid
|
| 17 |
- lfm2
|
| 18 |
- lfm2.5
|
|
|
|
| 22 |
|
| 23 |
[LiquidAI/LFM2.5-Encoder-350M-Prompt-Router](https://huggingface.co/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](https://huggingface.co/spaces/LiquidAI/prompt-routing)).
|
| 24 |
|
| 25 |
+
## Model description
|
| 26 |
+
|
| 27 |
+
| File | Recipe | Size | Target |
|
| 28 |
|---|---|---|---|
|
| 29 |
+
| `LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 365 MB | mobile + desktop |
|
| 30 |
| `LFM2.5-Encoder-350M-Prompt-Router_fp16.tflite` | fp16 weights, float compute | 713 MB | desktop — phone memory limits (XNNPACK per-signature fp32 unpacking) |
|
| 31 |
|
| 32 |
+
Two signatures, `route_128` and `route_512` (S = 128 / 512, batch 1, right-padded, up to **8 lane slots**):
|
| 33 |
|
| 34 |
+
| Tensor | Shape | Meaning |
|
|
|
|
|
|
|
| 35 |
|---|---|---|
|
| 36 |
+
| `input_ids` | int32 `[1, S]` | `Categories:\n- <lane 1>\n- <lane 2>…\n\nText:\n<prompt>` |
|
| 37 |
+
| `attention_mask` | int32 `[1, S]` | 1 = real token, 0 = pad |
|
| 38 |
+
| `text_pool` | float32 `[1, 1, S]` | mean-pool weights over the prompt's own tokens (`1/n` each) |
|
| 39 |
+
| `category_pool` | float32 `[1, 8, S]` | row *r* = mean-pool weights over lane *r*'s tokens; unused rows all-zero |
|
| 40 |
+
| output | float32 `[1, 8]` | one logit per lane slot |
|
| 41 |
+
|
| 42 |
+
Softmax over the first N (real) lanes only — an all-zero pool row produces a constant bias logit that must be ignored.
|
| 43 |
|
| 44 |
+
## How to use
|
| 45 |
|
| 46 |
+
**1. Install dependencies**
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
pip install ai-edge-litert numpy tokenizers huggingface_hub
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**2. Save the script** below as `route_prompt.py`:
|
| 53 |
|
| 54 |
```python
|
| 55 |
+
#!/usr/bin/env python3
|
| 56 |
+
"""Route a prompt to one of your lanes with litert-community/LFM2.5-Encoder-350M-Prompt-Router."""
|
| 57 |
+
import argparse
|
| 58 |
+
|
| 59 |
import numpy as np
|
| 60 |
+
from ai_edge_litert.interpreter import Interpreter
|
| 61 |
+
from huggingface_hub import hf_hub_download
|
| 62 |
from tokenizers import Tokenizer
|
| 63 |
|
| 64 |
+
REPO = "litert-community/LFM2.5-Encoder-350M-Prompt-Router"
|
| 65 |
+
MAX_LANES = 8
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def build_inputs(text, lanes, tokenizer, seq_len):
|
| 69 |
+
"""Builds input_ids/attention_mask plus the two mean-pool matrices."""
|
| 70 |
+
body = "\n".join(f"- {lane}" for lane in lanes)
|
| 71 |
prefix = f"Categories:\n{body}\n\nText:\n"
|
| 72 |
+
encoding = tokenizer.encode(prefix + text)
|
| 73 |
+
ids, offsets = encoding.ids, encoding.offsets
|
| 74 |
+
if len(ids) > seq_len:
|
| 75 |
+
raise SystemExit(f"{len(ids)} tokens exceed --seq-len {seq_len}")
|
| 76 |
+
|
| 77 |
+
input_ids = np.zeros((1, seq_len), np.int32)
|
| 78 |
+
attention_mask = np.zeros((1, seq_len), np.int32)
|
| 79 |
+
input_ids[0, : len(ids)] = ids
|
| 80 |
+
attention_mask[0, : len(ids)] = 1
|
| 81 |
+
|
| 82 |
+
# Mean-pool over the document's own tokens.
|
| 83 |
+
text_pool = np.zeros((1, 1, seq_len), np.float32)
|
| 84 |
+
text_idx = [i for i, (a, b) in enumerate(offsets) if b > len(prefix) and a != b]
|
| 85 |
+
text_pool[0, 0, text_idx] = 1 / len(text_idx)
|
| 86 |
+
|
| 87 |
+
# Mean-pool over each lane's tokens; unused lane rows stay all-zero.
|
| 88 |
+
category_pool = np.zeros((1, MAX_LANES, seq_len), np.float32)
|
| 89 |
pos = len("Categories:\n")
|
| 90 |
for r, lane in enumerate(lanes):
|
| 91 |
+
start, end = pos + 2, pos + 2 + len(lane)
|
| 92 |
+
pos = end + 1
|
| 93 |
+
idx = [i for i, (a, b) in enumerate(offsets) if a < end and b > start and a != b]
|
| 94 |
+
category_pool[0, r, idx] = 1 / len(idx)
|
| 95 |
+
|
| 96 |
+
return {
|
| 97 |
+
"input_ids": input_ids,
|
| 98 |
+
"attention_mask": attention_mask,
|
| 99 |
+
"text_pool": text_pool,
|
| 100 |
+
"category_pool": category_pool,
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def main():
|
| 105 |
+
parser = argparse.ArgumentParser()
|
| 106 |
+
parser.add_argument("--text", required=True, help="The prompt to route.")
|
| 107 |
+
parser.add_argument("--lane", action="append", required=True,
|
| 108 |
+
help="A routing lane, repeatable (up to 8).")
|
| 109 |
+
parser.add_argument("--seq-len", type=int, default=512, choices=[128, 512])
|
| 110 |
+
args = parser.parse_args()
|
| 111 |
+
if len(args.lane) > MAX_LANES:
|
| 112 |
+
raise SystemExit(f"at most {MAX_LANES} lanes")
|
| 113 |
+
|
| 114 |
+
model_path = hf_hub_download(REPO, "LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite")
|
| 115 |
+
tokenizer = Tokenizer.from_file(hf_hub_download(REPO, "tokenizer.json"))
|
| 116 |
+
|
| 117 |
+
feed = build_inputs(args.text, args.lane, tokenizer, args.seq_len)
|
| 118 |
+
interpreter = Interpreter(model_path=model_path)
|
| 119 |
+
runner = interpreter.get_signature_runner(f"route_{args.seq_len}")
|
| 120 |
+
logits = list(runner(**feed).values())[0][0]
|
| 121 |
+
|
| 122 |
+
# Softmax over the real lanes only — unused rows carry a constant bias logit.
|
| 123 |
+
real = logits[: len(args.lane)]
|
| 124 |
+
probs = np.exp(real - real.max())
|
| 125 |
+
probs /= probs.sum()
|
| 126 |
+
for lane, p in sorted(zip(args.lane, probs), key=lambda x: -x[1]):
|
| 127 |
+
print(f"{p:6.3f} {lane}")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
main()
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
**3. Run it**
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
python route_prompt.py \
|
| 138 |
+
--text "My Python script throws a KeyError on a dict lookup, how do I fix it?" \
|
| 139 |
+
--lane "coding question" --lane "travel planning" \
|
| 140 |
+
--lane "medical advice" --lane "small talk"
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
```
|
| 144 |
+
0.838 coding question
|
| 145 |
+
0.054 small talk
|
| 146 |
+
0.054 travel planning
|
| 147 |
+
0.054 medical advice
|
| 148 |
```
|
| 149 |
|
| 150 |
+
On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature names; the tokenizer is the standard Hugging Face `tokenizer.json`.
|
| 151 |
+
|
| 152 |
+
## Performance
|
| 153 |
+
|
| 154 |
+
One pass over a padded sequence with the int8 (`wi8fc`) file, CPU only.
|
| 155 |
+
|
| 156 |
+
| Device | Threads | `route_128` | `route_512` |
|
| 157 |
+
|---|---|---|---|
|
| 158 |
+
| Apple M4 Max (macOS) | 8 | 34.5 ms | 112.3 ms |
|
| 159 |
+
| iPhone 17 Pro | 6 | not measured | 145 ms |
|
| 160 |
+
|
| 161 |
+
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.
|
| 162 |
+
|
| 163 |
+
**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.
|
| 164 |
+
|
| 165 |
+
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.
|
| 166 |
+
|
| 167 |
+
### Accuracy note
|
| 168 |
+
|
| 169 |
+
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.
|
| 170 |
|
| 171 |
+
On the iPhone 17 Pro the int8 file reproduces the desktop outputs **bit-exactly** — cosine 1.000000, max absolute difference 0.0.
|
| 172 |
|
| 173 |
## License
|
| 174 |
|