mlboydaisuke commited on
Commit
3caec5d
·
verified ·
1 Parent(s): 3ee2a4b

Card: measured Performance table (M4 Max + iPhone 17 Pro), verified runnable usage script, accuracy note

Browse files
Files changed (1) hide show
  1. README.md +130 -29
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
- | File | Recipe | Size | |
 
 
26
  |---|---|---|---|
27
- | `LFM2.5-Encoder-350M-Prompt-Router_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 365 MB | mobile + desktop (iPhone-verified bit-exact, 145 ms) |
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
- ## Signatures
31
 
32
- `route_128` / `route_512` (S = 128 / 512, batch 1, right-padded, up to **8 lane slots**):
33
-
34
- | Input | Shape | |
35
  |---|---|---|
36
- | `input_ids` | int32 `[1, S]` | prompt tokens: `Categories:\n- <lane 1>\n- <lane 2>…\n\nText:\n<prompt>` |
37
- | `attention_mask` | int32 `[1, S]` | 1 = token, 0 = pad |
38
- | `text_pool` | float32 `[1, 1, S]` | mean-pool weights over the prompt's text tokens (`1/n` each) |
39
- | `category_pool` | float32 `[1, 8, S]` | row r = mean-pool weights over lane r's tokens; unused lane rows all-zero |
 
 
 
40
 
41
- Output: `logits` float32 `[1, 8]`. Softmax over the first N (real) lanes only — all-zero pool rows produce a constant bias logit that must be ignored.
42
 
43
- The pool matrices are built host-side from tokenizer character offsets, exactly like the base repo's `route()` helper:
 
 
 
 
 
 
44
 
45
  ```python
 
 
 
 
46
  import numpy as np
 
 
47
  from tokenizers import Tokenizer
48
 
49
- def build_inputs(text, lanes, tok, S=512):
50
- body = "\n".join(f"- {r}" for r in lanes)
 
 
 
 
 
51
  prefix = f"Categories:\n{body}\n\nText:\n"
52
- enc = tok.encode(prefix + text)
53
- ids, offs = enc.ids, enc.offsets
54
- x = np.zeros((1, S), np.int32); m = np.zeros((1, S), np.int32)
55
- x[0, :len(ids)] = ids; m[0, :len(ids)] = 1
56
- tp = np.zeros((1, 1, S), np.float32)
57
- ti = [i for i, (a, b) in enumerate(offs) if b > len(prefix) and a != b]
58
- tp[0, 0, ti] = 1 / len(ti)
59
- cp = np.zeros((1, 8, S), np.float32)
 
 
 
 
 
 
 
 
 
60
  pos = len("Categories:\n")
61
  for r, lane in enumerate(lanes):
62
- a, b = pos + 2, pos + 2 + len(lane); pos = b + 1
63
- idx = [i for i, (ta, tb) in enumerate(offs) if ta < b and tb > a and ta != tb]
64
- cp[0, r, idx] = 1 / len(idx)
65
- return {"input_ids": x, "attention_mask": m, "text_pool": tp, "category_pool": cp}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  ```
67
 
68
- ## Verification
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- Task-level parity vs the PyTorch reference (demo prompt, 4 lanes): fp32, fp16 **and int8 all reproduce the reference lane probabilities to 4 decimal places** ("coding question" 0.838). On an iPhone 17 Pro the int8 file reproduces the desktop outputs **bit-exactly** (cosine 1.000000, max diff 0.0) at 145 ms per `route_512` pass (6 threads, XNNPACK).
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