mlboydaisuke commited on
Commit
c217a2c
·
verified ·
1 Parent(s): 16e04aa

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 +105 -31
README.md CHANGED
@@ -37,62 +37,136 @@ tags:
37
 
38
  # LFM2.5-Encoder-350M — LiteRT
39
 
40
- [LiquidAI/LFM2.5-Encoder-350M](https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M) converted to **LiteRT** (`.tflite`) for on-device inference. A multilingual (15 languages) bidirectional encoder on the LFM2 hybrid backbone (gated short-convolutions + grouped-query attention) — use it for embeddings, retrieval, classification heads, and masked-token prediction, fully offline on CPU.
41
 
42
- | File | Recipe | Size | |
 
 
43
  |---|---|---|---|
44
- | `LFM2.5-Encoder-350M_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 376 MB | mobile + desktop (iPhone-verified bit-exact) |
45
  | `LFM2.5-Encoder-350M_fp16.tflite` | fp16 weights, float compute | 713 MB | desktop — XNNPACK's per-signature fp32 unpacking exceeds iPhone memory limits |
46
 
47
- ## Signatures
48
-
49
- All signatures take batch-1, right-padded static shapes: `input_ids` int32 `[1, S]`, `attention_mask` int32 `[1, S]` (1 = real token, 0 = pad).
50
 
51
  | Signature | Output |
52
  |---|---|
53
  | `encode_64` / `encode_128` / `encode_256` / `encode_512` | `last_hidden_state` float32 `[1, S, 1024]`, zeroed at padded positions |
54
  | `mlm_128` | masked-LM logits float32 `[1, 128, 65536]` |
55
 
56
- Padded positions are fully masked inside the graph (conv path and attention), so the output at valid positions is independent of padding length — `encode_64/128/256` agree bitwise on the same sentence, and match the unpadded PyTorch reference.
57
 
58
- ## Quality (parity vs PyTorch fp32 reference)
59
 
60
- 16 sentences covering all 15 supported languages; mean-pooled sentence embedding cosine vs the original `Lfm2BidirectionalModel`, and top-5 fill-mask agreement on en/fr/de/ja cloze prompts:
61
 
62
- | Variant | Pooled cos (min / mean) | Per-token corr (min) | Fill-mask |
63
- |---|---|---|---|
64
- | fp16 | 1.000000 / 1.000000 | 1.000000 | top-5 sets identical (4/4 prompts) |
65
- | int8 (wi8fc) | 0.996563 / 0.998592 | 0.990448 | top-1 3/4, ≥3/5 top-5 overlap on all |
66
-
67
- ## Speed (CPU/XNNPACK)
68
-
69
- | Variant | Device | encode_128 | encode_512 |
70
- |---|---|---|---|
71
- | int8 (wi8fc) | Apple-silicon Mac (all threads) | 46 ms (2765 tok/s) | 123 ms (4180 tok/s) |
72
- | int8 (wi8fc) | iPhone 17 Pro (6 threads) | 42 ms | 145 ms (~3500 tok/s) |
73
 
74
- On the iPhone 17 Pro the int8 model reproduces the Mac outputs **bit-exactly** (cosine 1.000000, max diff 0.0) across all tested languages and signatures; peak footprint ≈1.65 GiB.
 
 
75
 
76
- ## Usage (Python)
77
 
78
  ```python
 
 
 
 
79
  import numpy as np
80
  from ai_edge_litert.interpreter import Interpreter
 
81
  from tokenizers import Tokenizer
82
 
83
- tok = Tokenizer.from_file("tokenizer.json")
84
- it = Interpreter(model_path="LFM2.5-Encoder-350M_wi8fc.tflite")
85
- encode = it.get_signature_runner("encode_128")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- ids = tok.encode("On-device embeddings, private and fast.").ids
88
- x = np.zeros((1, 128), np.int32); m = np.zeros((1, 128), np.int32)
89
- x[0, :len(ids)] = ids; m[0, :len(ids)] = 1
90
 
91
- h = list(encode(input_ids=x, attention_mask=m).values())[0] # [1, 128, 1024]
92
- emb = h[0, :len(ids)].mean(axis=0) # sentence embedding
 
 
 
93
  ```
94
 
95
- For masked-token prediction use the `mlm_128` signature and read the logits at the `[MASK]` position. On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature names; the tokenizer is the standard Hugging Face `tokenizer.json` (works with the `tokenizers` libraries for Rust/Swift/Kotlin).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  ## License
98
 
 
37
 
38
  # LFM2.5-Encoder-350M — LiteRT
39
 
40
+ [LiquidAI/LFM2.5-Encoder-350M](https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M) converted to **LiteRT** (`.tflite`) for on-device inference. A multilingual (15 languages) bidirectional encoder on the LFM2 hybrid backbone — gated short-convolutions plus grouped-query attention — for embeddings, retrieval, classification heads, and masked-token prediction, fully offline on CPU.
41
 
42
+ ## Model description
43
+
44
+ | File | Recipe | Size | Target |
45
  |---|---|---|---|
46
+ | `LFM2.5-Encoder-350M_wi8fc.tflite` | int8 dynamic-range (linears + embedding, convs float) | 376 MB | mobile + desktop |
47
  | `LFM2.5-Encoder-350M_fp16.tflite` | fp16 weights, float compute | 713 MB | desktop — XNNPACK's per-signature fp32 unpacking exceeds iPhone memory limits |
48
 
49
+ All signatures take batch-1, right-padded static shapes: `input_ids` int32 `[1, S]` and `attention_mask` int32 `[1, S]` (1 = real token, 0 = pad).
 
 
50
 
51
  | Signature | Output |
52
  |---|---|
53
  | `encode_64` / `encode_128` / `encode_256` / `encode_512` | `last_hidden_state` float32 `[1, S, 1024]`, zeroed at padded positions |
54
  | `mlm_128` | masked-LM logits float32 `[1, 128, 65536]` |
55
 
56
+ Padded positions are fully masked inside the graph, in both the convolution path and attention, so the output at valid positions does not depend on how much padding follows: `encode_64`, `encode_128` and `encode_256` agree bitwise on the same sentence and match the unpadded PyTorch reference.
57
 
58
+ For a smaller sibling see [LFM2.5-Encoder-230M](https://huggingface.co/litert-community/LFM2.5-Encoder-230M).
59
 
60
+ ## How to use
61
 
62
+ **1. Install dependencies**
 
 
 
 
 
 
 
 
 
 
63
 
64
+ ```bash
65
+ pip install ai-edge-litert numpy tokenizers huggingface_hub
66
+ ```
67
 
68
+ **2. Save the script** below as `embed.py`:
69
 
70
  ```python
71
+ #!/usr/bin/env python3
72
+ """Embed sentences with litert-community/LFM2.5-Encoder-350M and rank them by similarity."""
73
+ import argparse
74
+
75
  import numpy as np
76
  from ai_edge_litert.interpreter import Interpreter
77
+ from huggingface_hub import hf_hub_download
78
  from tokenizers import Tokenizer
79
 
80
+ REPO = "litert-community/LFM2.5-Encoder-350M"
81
+ MODEL_FILE = "LFM2.5-Encoder-350M_wi8fc.tflite"
82
+
83
+
84
+ def embed(runner, tokenizer, text, seq_len):
85
+ """Mean-pools the encoder states over the real tokens into one vector."""
86
+ ids = tokenizer.encode(text).ids
87
+ if len(ids) > seq_len:
88
+ raise SystemExit(f"{len(ids)} tokens exceed --seq-len {seq_len}")
89
+ input_ids = np.zeros((1, seq_len), np.int32)
90
+ attention_mask = np.zeros((1, seq_len), np.int32)
91
+ input_ids[0, : len(ids)] = ids
92
+ attention_mask[0, : len(ids)] = 1
93
+ states = list(runner(input_ids=input_ids, attention_mask=attention_mask).values())[0]
94
+ vector = states[0, : len(ids)].mean(axis=0)
95
+ return vector / np.linalg.norm(vector)
96
+
97
+
98
+ def main():
99
+ parser = argparse.ArgumentParser()
100
+ parser.add_argument("--query", required=True, help="The sentence to match.")
101
+ parser.add_argument("--candidate", action="append", required=True,
102
+ help="A candidate sentence, repeatable.")
103
+ parser.add_argument("--seq-len", type=int, default=128,
104
+ choices=[64, 128, 256, 512])
105
+ parser.add_argument("--threads", type=int, default=8)
106
+ args = parser.parse_args()
107
+
108
+ model_path = hf_hub_download(REPO, MODEL_FILE)
109
+ tokenizer = Tokenizer.from_file(hf_hub_download(REPO, "tokenizer.json"))
110
+ interpreter = Interpreter(model_path=model_path, num_threads=args.threads)
111
+ runner = interpreter.get_signature_runner(f"encode_{args.seq_len}")
112
+
113
+ query = embed(runner, tokenizer, args.query, args.seq_len)
114
+ scored = [(float(query @ embed(runner, tokenizer, c, args.seq_len)), c)
115
+ for c in args.candidate]
116
+ for score, text in sorted(scored, reverse=True):
117
+ print(f"{score:6.3f} {text}")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
122
+ ```
123
 
124
+ **3. Run it**
 
 
125
 
126
+ ```bash
127
+ python embed.py --query "What is your refund policy?" \
128
+ --candidate "Our refund policy allows 30 days" \
129
+ --candidate "Steps to recover a forgotten login" \
130
+ --candidate "The weather in Osaka is mild in spring"
131
  ```
132
 
133
+ ```
134
+ 0.604 Our refund policy allows 30 days
135
+ 0.579 The weather in Osaka is mild in spring
136
+ 0.570 Steps to recover a forgotten login
137
+ ```
138
+
139
+ Mean-pooling the raw encoder states is the simplest sentence representation and is what the numbers above use; for retrieval at quality you would normally train a pooling head or fine-tune on your own pairs. For masked-token prediction use the `mlm_128` signature and read the logits at the `[MASK]` position.
140
+
141
+ On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature names; the tokenizer is the standard Hugging Face `tokenizer.json`, which the Rust/Swift/Kotlin `tokenizers` bindings all read.
142
+
143
+ ## Performance
144
+
145
+ int8 (`wi8fc`) file, CPU only.
146
+
147
+ | Device | Threads | `encode_128` | `encode_512` | `mlm_128` |
148
+ |---|---|---|---|---|
149
+ | Apple M4 Max (macOS) | 8 | 36.5 ms | 111.2 ms | 44.7 ms |
150
+ | iPhone 17 Pro | 6 | 42 ms | 145 ms | 73 ms |
151
+
152
+ Mac figures are the median of 20 warm runs (`ai-edge-litert` 2.1.6, XNNPACK, otherwise idle machine). iPhone figures come from the on-device gate (TFLite C API + SignatureRunner + XNNPACK); each is the last of three consecutive measurements, not a median.
153
+
154
+ **Budget for one slow first call.** The first inference after loading pays a one-time graph preparation. On the Mac that first call took 908 ms against a 36.5 ms steady state; on the iPhone the three consecutive `encode_128` measurements were 102, 51 and 42 ms, and the three `encode_512` measurements were 229, 152 and 145 ms. Model load was 1.48 s on the iPhone, with a peak footprint of about 1.65 GiB.
155
+
156
+ Those three consecutive values are **not** a language effect — the signatures are fixed-shape, so every input of a given signature costs the same. Measured warm on the Mac with the run order reversed, one signature takes 36.4 / 37.3 / 36.6 ms on English, Japanese and Arabic sentences of 17, 21 and 27 tokens.
157
+
158
+ Thread count matters more than anything else here: at the interpreter default this model measures 52.4 ms and 227.1 ms for `encode_128` and `encode_512`, against 36.5 ms and 111.2 ms at 8 threads.
159
+
160
+ ### Accuracy note
161
+
162
+ Parity against the PyTorch fp32 reference over 16 sentences covering all 15 supported languages — mean-pooled sentence-embedding cosine against the original `Lfm2BidirectionalModel`, plus top-5 fill-mask agreement on English, French, German and Japanese cloze prompts:
163
+
164
+ | Variant | Pooled cosine (min / mean) | Per-token correlation (min) | Fill-mask |
165
+ |---|---|---|---|
166
+ | fp16 | 1.000000 / 1.000000 | 1.000000 | top-5 sets identical (4/4 prompts) |
167
+ | int8 (`wi8fc`) | 0.996563 / 0.998592 | 0.990448 | top-1 on 3/4, at least 3/5 top-5 overlap on all |
168
+
169
+ On the iPhone 17 Pro the int8 file reproduces the Mac outputs **bit-exactly** — cosine 1.000000, max absolute difference 0.0 — across every tested language and signature.
170
 
171
  ## License
172