File size: 7,300 Bytes
3ee2a4b
 
 
 
 
 
 
 
 
 
 
 
 
 
3caec5d
3ee2a4b
 
 
 
 
 
 
 
 
3caec5d
 
 
3ee2a4b
3caec5d
3ee2a4b
 
3caec5d
3ee2a4b
3caec5d
3ee2a4b
3caec5d
 
 
 
 
 
 
3ee2a4b
3caec5d
3ee2a4b
3caec5d
 
 
 
 
 
 
3ee2a4b
 
3caec5d
 
 
 
3ee2a4b
3caec5d
 
3ee2a4b
 
3caec5d
 
 
 
 
 
 
3ee2a4b
3caec5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ee2a4b
 
3caec5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ee2a4b
 
3caec5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ee2a4b
3caec5d
3ee2a4b
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
---
license: other
license_name: lfm1.0
license_link: LICENSE
base_model: LiquidAI/LFM2.5-Encoder-350M-Prompt-Router
pipeline_tag: text-classification
library_name: litert
tags:
- litert
- tflite
- on-device
- edge
- encoder
- routing
- zero-shot
- liquid
- lfm2
- lfm2.5
---

# LFM2.5-Encoder-350M-Prompt-Router — LiteRT

[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)).

## Model description

| File | Recipe | Size | Target |
|---|---|---|---|
| `LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 365 MB | mobile + desktop |
| `LFM2.5-Encoder-350M-Prompt-Router_fp16.tflite` | fp16 weights, float compute | 713 MB | desktop — phone memory limits (XNNPACK per-signature fp32 unpacking) |

Two signatures, `route_128` and `route_512` (S = 128 / 512, batch 1, right-padded, up to **8 lane slots**):

| Tensor | Shape | Meaning |
|---|---|---|
| `input_ids` | int32 `[1, S]` | `Categories:\n- <lane 1>\n- <lane 2>…\n\nText:\n<prompt>` |
| `attention_mask` | int32 `[1, S]` | 1 = real token, 0 = pad |
| `text_pool` | float32 `[1, 1, S]` | mean-pool weights over the prompt's own tokens (`1/n` each) |
| `category_pool` | float32 `[1, 8, S]` | row *r* = mean-pool weights over lane *r*'s tokens; unused rows all-zero |
| output | float32 `[1, 8]` | one logit per lane slot |

Softmax over the first N (real) lanes only — an all-zero pool row produces a constant bias logit that must be ignored.

## How to use

**1. Install dependencies**

```bash
pip install ai-edge-litert numpy tokenizers huggingface_hub
```

**2. Save the script** below as `route_prompt.py`:

```python
#!/usr/bin/env python3
"""Route a prompt to one of your lanes with litert-community/LFM2.5-Encoder-350M-Prompt-Router."""
import argparse

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-Prompt-Router"
MAX_LANES = 8


def build_inputs(text, lanes, tokenizer, seq_len):
    """Builds input_ids/attention_mask plus the two mean-pool matrices."""
    body = "\n".join(f"- {lane}" for lane in lanes)
    prefix = f"Categories:\n{body}\n\nText:\n"
    encoding = tokenizer.encode(prefix + text)
    ids, offsets = encoding.ids, encoding.offsets
    if len(ids) > seq_len:
        raise SystemExit(f"{len(ids)} tokens exceed --seq-len {seq_len}")

    input_ids = np.zeros((1, seq_len), np.int32)
    attention_mask = np.zeros((1, seq_len), np.int32)
    input_ids[0, : len(ids)] = ids
    attention_mask[0, : len(ids)] = 1

    # Mean-pool over the document's own tokens.
    text_pool = np.zeros((1, 1, seq_len), np.float32)
    text_idx = [i for i, (a, b) in enumerate(offsets) if b > len(prefix) and a != b]
    text_pool[0, 0, text_idx] = 1 / len(text_idx)

    # Mean-pool over each lane's tokens; unused lane rows stay all-zero.
    category_pool = np.zeros((1, MAX_LANES, seq_len), np.float32)
    pos = len("Categories:\n")
    for r, lane in enumerate(lanes):
        start, end = pos + 2, pos + 2 + len(lane)
        pos = end + 1
        idx = [i for i, (a, b) in enumerate(offsets) if a < end and b > start and a != b]
        category_pool[0, r, idx] = 1 / len(idx)

    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "text_pool": text_pool,
        "category_pool": category_pool,
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--text", required=True, help="The prompt to route.")
    parser.add_argument("--lane", action="append", required=True,
                        help="A routing lane, repeatable (up to 8).")
    parser.add_argument("--seq-len", type=int, default=512, choices=[128, 512])
    args = parser.parse_args()
    if len(args.lane) > MAX_LANES:
        raise SystemExit(f"at most {MAX_LANES} lanes")

    model_path = hf_hub_download(REPO, "LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite")
    tokenizer = Tokenizer.from_file(hf_hub_download(REPO, "tokenizer.json"))

    feed = build_inputs(args.text, args.lane, tokenizer, args.seq_len)
    interpreter = Interpreter(model_path=model_path)
    runner = interpreter.get_signature_runner(f"route_{args.seq_len}")
    logits = list(runner(**feed).values())[0][0]

    # Softmax over the real lanes only — unused rows carry a constant bias logit.
    real = logits[: len(args.lane)]
    probs = np.exp(real - real.max())
    probs /= probs.sum()
    for lane, p in sorted(zip(args.lane, probs), key=lambda x: -x[1]):
        print(f"{p:6.3f}  {lane}")


if __name__ == "__main__":
    main()
```

**3. Run it**

```bash
python route_prompt.py \
  --text "My Python script throws a KeyError on a dict lookup, how do I fix it?" \
  --lane "coding question" --lane "travel planning" \
  --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.

## 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-Prompt-Router with modification notices per Section 4; all credit for the model to [Liquid AI](https://www.liquid.ai/).