iannedev commited on
Commit
dfe210b
·
verified ·
1 Parent(s): acae005
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model/embeddinggemma-2-text.aimodel/main.mlirb filter=lfs diff=lfs merge=lfs -text
37
+ model/tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## 1.0 — 2026-10-12
4
+
5
+ First release package.
6
+
7
+ - Model: `model/embeddinggemma-2-text.aimodel`, the text encoder of google/embeddinggemma-2 on Apple Core AI
8
+ (macOS 27), fp16, every function one Neural Engine region plus the token lookup; buckets 32…2048.
9
+ Parity vs the fp32 reference on 357 fictional texts: cosine min 0.99992, median 0.99997 (min 0.99985 on a
10
+ second, independent set of 100 texts: expect >= 0.9998); retrieval ranks
11
+ identical at 768 dims on a 1,712-chunk / 53-query fictional benchmark.
12
+ - Swift: `EmbeddingGemma2CoreAI` library (no third-party dependencies) with an exact Gemma tokenizer
13
+ (`GemmaTokenizer`, byte-identical to Hugging Face `tokenizers` on 3,758 fixture texts / 608,225 ids) and
14
+ the `eg2-cli` tool (embed, smoke, tokcheck, latency, prewarm, cleanup-temp).
15
+ - Python: `embeddinggemma2_coreai.py` runner (library + CLI) using the `tokenizers` library, `smoke_test.py`,
16
+ pinned requirements.
17
+ - Tests: `tests/smoke-reference.json` (3 fictional sentences, fp32 reference vectors),
18
+ `tests/tokenizer-cases.json` (3,758 fictional texts with reference ids) and the generators for both.
19
+ - Exporter: `export/eg2_text.py` + `export/export.py` reproduce the bundle from the original checkpoint.
20
+ - Temp-file hygiene: both runners can delete the MPSGraph temp folder their own process created
21
+ (`--cleanup-temp`, `removeOwnMPSGraphTempFiles()`, `remove_own_mpsgraph_temp_files()`).
22
+ - Licensing: full Apache-2.0 text, NOTICE with the modification list and the Gemma Prohibited Use Policy
23
+ pointer, SHA-256 checksums of every file.
24
+
25
+ Changes relative to the internal validation build that preceded this package:
26
+
27
+ - The global-attention layers use the 4-D matmul form (the earlier bundle fell back to the GPU on most
28
+ buckets); the earlier bundle is not included.
29
+ - The Swift runner no longer depends on swift-transformers (whose BPE differed from Hugging Face on CRLF,
30
+ decomposed accents, emoji sequences, Arabic diacritics, Thai and whitespace runs longer than 31).
31
+ - tokenizer.json is read with an exact JSON reader: Foundation's JSONSerialization drops a leading U+FEFF from
32
+ string values, which corrupted three vocabulary entries.
CHECKSUMS.sha256 ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 7c059597beb76a3ce4e7cc6e0b08f6aa7e8eeb932ab9b18e13234ca9537809e1 CHANGELOG.md
2
+ cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE
3
+ dfaaa2df3d3b356d6ab7d37795893ca3916f9855095658700b35f387f8e7d68f NOTICE
4
+ 742bc793350bbe91c402dcdbc339403f671b46a8c953d366a41f803300b3f52b README.md
5
+ bb778eee49dab44fb997f67c875a615d754d54477e257d02771e94f250b4cd38 export/eg2_text.py
6
+ 9392857facca6d9b9a735146c72a342ffa8c52c7c752ea2c499acb10686a3cdc export/export.py
7
+ 7ddb494420af4401b1e3c6e0b54f176c218f9d5068079c9ccfa7871fad5f5a19 model/MODEL-ATTRIBUTION.json
8
+ 1791655ef7f1c0c093d200904263989c0f7cfc637869b71c6fc5f90f0f800f18 model/config.json
9
+ c2de75ebee766df0e980f63bc56a60847272cfe393de744e766bee9ba1a8049b model/embeddinggemma-2-text.aimodel/main.hash
10
+ 75d38431abcd2471b0540c863357710d305cb1687295f5b0cf859de6ac9d46eb model/embeddinggemma-2-text.aimodel/main.mlirb
11
+ c2362545fc6b46be69f394ff0081a6e919a9099b19a84b167fbdeb71ba7c793c model/embeddinggemma-2-text.aimodel/metadata.json
12
+ 4d777ef5bdc1aa36227abdfb77c3e49e7b9c892d16e1b6bda41c393504828be4 model/tokenizer/tokenizer.json
13
+ 17bd5d6e9364ca49a534e1502076593317c298d4a663623091ed45388f004874 model/tokenizer/tokenizer_config.json
14
+ 2559a5e111a31b53e9568c962be2fb51ec84c17912bcfc40f0cafb7f73ca9f01 python/embeddinggemma2_coreai.py
15
+ 3bc2e5931e7dde258a82637e082bf9f12f9a6eed6c4cc703f9b8e4e02a9d3b0c python/requirements.lock
16
+ 2d7b80b555b56787f2a142da1164c359e306b7ad3332661ac158b3a1f114d581 python/requirements.txt
17
+ 3126ad6f90704e025f57632664fe1954a7c88b734a9e695441f471dff79d863e python/smoke_test.py
18
+ de934714b85d008c8e56b8027c86c0f1319bb3ca1bb0dec9ffe3ab7ca8c35876 swift/Package.swift
19
+ 5c36c78cf268da78c3a6981bcfd4974b94641f756fa9616950cd39ebe6fc096f swift/Sources/EmbeddingGemma2CoreAI/Embedder.swift
20
+ 7b99edfefc47a343d07df32a3b06a8da2f94a49ae0a8e299e733c1a44c09e38f swift/Sources/EmbeddingGemma2CoreAI/ExactJSON.swift
21
+ 4f5ef8a454a84937e503ba99c9f05e237347178933a442f2352a2c5db0ad87eb swift/Sources/EmbeddingGemma2CoreAI/GemmaTokenizer.swift
22
+ f59a75b9eb311777bba3f8b1146b75a11f6587cdca5a2c1dab6fffc40d96a9aa swift/Sources/eg2-cli/main.swift
23
+ 1df8f74c85f4a02be0caaf4b99ee0bb8f4cfc1759ad091fdf65a98fdea917df8 tests/make_smoke_reference.py
24
+ 07daa7415beb15e9049a307aa17bf048ba2edaa0613301293e6250a66ed552b8 tests/make_tokenizer_cases.py
25
+ dce930ac23f448c55d3e4fbf5e3d1080213395510637d627f67b54a889f339e1 tests/smoke-reference.json
26
+ 0797f417a0fa96270dc2f439478000fe3b3a235eb1e83b3054cf69674e323ae9 tests/tokenizer-cases.json
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
NOTICE ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ EmbeddingGemma-2-CoreAI-text v1.0
2
+ =================================
3
+
4
+ This package contains a derivative of the EmbeddingGemma 2 model.
5
+
6
+ EmbeddingGemma 2
7
+ Authors: Google DeepMind (as stated on the model card)
8
+ https://huggingface.co/google/embeddinggemma-2
9
+ License as linked by the model card: Apache 2.0, https://ai.google.dev/gemma/docs/gemma_4_license
10
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use these files except in
11
+ compliance with the License. A copy of the License is in the file LICENSE and at
12
+ http://www.apache.org/licenses/LICENSE-2.0.
13
+ Unless required by applicable law or agreed to in writing, software distributed under the License is
14
+ distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+
16
+ The model card of google/embeddinggemma-2 asks that deployments adhere to the Gemma Prohibited Use Policy:
17
+ https://ai.google.dev/gemma/prohibited_use_policy. Please read it before using this model in a product.
18
+
19
+ The tokenizer files in model/tokenizer/ (tokenizer.json, tokenizer_config.json) are copied unchanged from
20
+ google/embeddinggemma-2 and are covered by the same license and attribution.
21
+
22
+
23
+ MODIFICATIONS (Apache License, Section 4(b))
24
+ --------------------------------------------
25
+
26
+ Changes made to google/embeddinggemma-2, 11-12 October 2026, to produce model/embeddinggemma-2-text.aimodel:
27
+
28
+ 2026-10-11 Text encoder only. The vision and audio encoders of the multimodal checkpoint were dropped.
29
+ 2026-10-11 The encoder architecture (24 bidirectional Gemma 4 layers, per-layer embeddings, RMSNorm with
30
+ max-abs rescaling, RoPE, GQA/MQA attention, GELU-tanh MLP, final RMSNorm, 512->768 projection,
31
+ masked mean pooling, L2 normalization) was re-authored in plain PyTorch for the Core AI
32
+ converter. The weights are the original ones, rounded from bfloat16 to float16; the sqrt(512)
33
+ token-embedding scale and the 512^-0.5 per-layer-embedding scale are folded into the tables.
34
+ 2026-10-11 fp16 handling for the Neural Engine: every RMSNorm and the final L2 normalization divide the row
35
+ by its max-abs value before squaring (activations reach ~2000, whose square overflows fp16);
36
+ in the model code every division is expressed as rsqrt(x)*rsqrt(x); the additive attention-mask
37
+ value is -10000. (The model card advises against float16; this rescaling is what makes fp16
38
+ safe here, see the parity figures in README.md.)
39
+ 2026-10-11 Pooling, projection and normalization run inside the graph; the output is the final embedding.
40
+ 2026-10-11 Fixed sequence buckets: one function main_<S> per S in 32, 64, 128, 256, 512, 1024, 2048.
41
+ Input longer than 2048 tokens is not supported by this build (the original accepts 8192).
42
+ 2026-10-12 Attention of the four global (full-attention, single key/value head) layers rewritten from a
43
+ 5-D grouped matmul to an equivalent plain 4-D matmul with the key/value tensors broadcast over
44
+ the four query heads, so that the Neural Engine compiler accepts the whole graph. Mathematically
45
+ identical to the reference (fp32 max abs diff 3e-7).
46
+ 2026-10-12 Converted with coreai-torch 0.4.3 / coreai-core 1.0.0b3 on torch 2.9.0 to the Core AI
47
+ .aimodel format (macOS 27).
48
+
49
+ The exporter in export/ reproduces these changes from the original checkpoint.
50
+
51
+
52
+ THIRD-PARTY ATTRIBUTIONS
53
+ ------------------------
54
+
55
+ HF transformers (Apache-2.0, Copyright 2026 The HuggingFace Team). export/eg2_text.py re-authors the
56
+ architecture described by modeling_embedding_gemma2.py / configuration_embedding_gemma2.py from
57
+ transformers 5.19. No transformers source files are included in this package; the re-authored module carries
58
+ its own Apache-2.0 header and credits transformers.
59
+
60
+ FluidInference Core ML port of EmbeddingGemma 2 (Apache-2.0, https://huggingface.co/FluidInference). The
61
+ idea of dividing each RMSNorm row by its max-abs value to keep fp16 in range was taken from the README of
62
+ that port and re-derived independently; no code was copied from it. Nothing else in this package derives from
63
+ it.
64
+
65
+ Apple Core AI (coreai-torch, coreai-core, the CoreAI framework): used as tools and runtime, not redistributed.
66
+ Hugging Face tokenizers (Apache-2.0): used by the Python runner and to generate the test fixtures; not
67
+ redistributed.
68
+
69
+
70
+ TRADEMARKS
71
+ ----------
72
+
73
+ Gemma and EmbeddingGemma are trademarks of Google LLC. Apple, Core AI, Apple silicon and Neural Engine are
74
+ trademarks of Apple Inc. This port is not affiliated with, sponsored by or endorsed by Google or Apple.
75
+
76
+
77
+ OUR OWN CODE
78
+ ------------
79
+
80
+ Everything in this package that is not the model weights or the tokenizer files, i.e. the exporter
81
+ (export/), the Python runner (python/), the Swift library and command-line tool (swift/), the tests (tests/)
82
+ and the documentation, is
83
+
84
+ Copyright 2026 Everpaper
85
+ Licensed under the Apache License, Version 2.0 (LICENSE).
README.md ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: google/embeddinggemma-2
4
+ library_name: coreai
5
+ pipeline_tag: feature-extraction
6
+ language:
7
+ - multilingual
8
+ tags:
9
+ - coreai
10
+ - apple-neural-engine
11
+ - on-device
12
+ - embeddings
13
+ - sentence-similarity
14
+ - gemma
15
+ - macos
16
+ ---
17
+
18
+ # EmbeddingGemma 2 (text) for Apple Core AI
19
+
20
+ The text encoder of [google/embeddinggemma-2](https://huggingface.co/google/embeddinggemma-2) (Gemma 4 family,
21
+ ~270 M parameters, 768-d Matryoshka embeddings) converted to Apple's **Core AI** runtime for **macOS 27 on Apple
22
+ silicon**. One graph does everything `SentenceTransformer("google/embeddinggemma-2").encode()` does for text: token
23
+ embedding, 24 bidirectional encoder layers, final norm, 512→768 projection, masked mean pooling and L2
24
+ normalization. Every function runs as one Neural Engine region plus the token lookup (a small GPU gather), so a
25
+ document costs about **0.3 ms of CPU** and **0.4–0.5 ms of GPU time**; the Neural Engine does the rest.
26
+
27
+ Ported by Everpaper — everpaper.app. Weights are the original ones (bf16 rounded to fp16). Apache-2.0, see
28
+ [LICENSE](LICENSE) and [NOTICE](NOTICE) (modification list, Gemma Prohibited Use Policy pointer).
29
+
30
+ ## Download
31
+
32
+ ```bash
33
+ hf download iannedev/embeddinggemma-2-coreai --local-dir EmbeddingGemma-2-CoreAI-text-v1.0
34
+ cd EmbeddingGemma-2-CoreAI-text-v1.0
35
+ ```
36
+
37
+ (`hf` comes with `pip install -U huggingface_hub`.) Then run one of the quick starts below; both begin with a
38
+ smoke test that checks the model on your machine.
39
+
40
+ ## What is in the package
41
+
42
+ ```
43
+ EmbeddingGemma-2-CoreAI-text-v1.0/
44
+ README.md, LICENSE, NOTICE, CHANGELOG.md, CHECKSUMS.sha256
45
+ model/
46
+ embeddinggemma-2-text.aimodel/ Core AI asset: functions main_32 … main_2048 (fp16, 631.5 MB = 602 MiB)
47
+ config.json buckets, prompts, special ids, Matryoshka dims
48
+ tokenizer/ tokenizer.json + tokenizer_config.json, unchanged from google/embeddinggemma-2
49
+ MODEL-ATTRIBUTION.json base model, checkpoint sha256, exporter versions, modification list
50
+ swift/ SwiftPM package: library `EmbeddingGemma2CoreAI` + tool `eg2-cli` (no dependencies)
51
+ python/ embeddinggemma2_coreai.py (library + CLI), smoke_test.py, requirements(.lock)
52
+ tests/ smoke-reference.json, tokenizer-cases.json (+ the scripts that generate them)
53
+ export/ eg2_text.py + export.py: reproduce the .aimodel from the original checkpoint
54
+ ```
55
+
56
+ ## Requirements
57
+
58
+ - macOS 27.0 or later on Apple silicon (the package was validated on an M5 with 24 GB; macOS 27.0 build
59
+ 26A5406e, Xcode 27 beta).
60
+ - Swift: Xcode 27 / Swift 6 toolchain; the `CoreAI` framework is part of the macOS 27 SDK.
61
+ - Python (runtime): Python 3.12 or 3.14 (both validated), Apple's `coreai-core` Python runtime (provides
62
+ `coreai.runtime`), `numpy`, `tokenizers` (`python/requirements.txt`; the exact validated versions are in
63
+ `python/requirements.lock`). Only the exporter (`export/`, optional) needs Python 3.12, for torch 2.9.0.
64
+ - Disk: 664 MB for `model/` (the `.aimodel` is 631.5 MB, the tokenizer 32 MB); the system keeps a per-executable
65
+ specialization cache of about 1.4 GB under `~/Library/Caches/coreai-cache/`.
66
+
67
+ ## Inputs and outputs
68
+
69
+ | | |
70
+ |---|---|
71
+ | Functions | `main_32`, `main_64`, `main_128`, `main_256`, `main_512`, `main_1024`, `main_2048` (one per sequence bucket S) |
72
+ | `input_ids` | `[1, S]` int32: `<bos>`=2, prompt-prefix tokens, text tokens, `<eos>`=1, right-padded with `<pad>`=0 |
73
+ | `attention_mask` | `[1, S]` float16: 1 for real tokens, 0 for padding |
74
+ | `embedding` | `[1, 768]` float16, already L2-normalized |
75
+ | Max length | 2048 tokens including `<bos>`/`<eos>` and the prefix. Longer input is **truncated** by the runners (the original model accepts 8192; chunk long documents at ≤ 512 tokens for retrieval, the embedding is a mean over tokens anyway). |
76
+ | Bucket choice | the smallest S ≥ token count. Padding does not change the result (verified: padded and unpadded runs are bit-identical). |
77
+ | Matryoshka | 512 / 256 / 128 dims: take the first N values and re-normalize (`embed(..., dim:)` does it). |
78
+
79
+ **Task prompts** (exactly the upstream `config_sentence_transformers.json`; the runners prepend them by `kind`):
80
+
81
+ | kind | prefix |
82
+ |---|---|
83
+ | `document` | `title: none \| text: ` — for titled documents use `title: {title} \| text: {content}` yourself |
84
+ | `search_query` | `task: search result \| query: ` |
85
+ | `question_answering` | `task: question answering \| query: ` |
86
+ | `fact_checking` | `task: fact checking \| query: ` |
87
+ | `code_retrieval` | `task: code retrieval \| query: ` |
88
+ | `classification` | `task: classification \| query: ` |
89
+ | `clustering` | `task: clustering \| query: ` |
90
+ | `sentence_similarity` | `task: sentence similarity \| query: ` |
91
+
92
+ The tokenizer is the original Gemma `tokenizer.json`. The runners add `<bos>` and `<eos>` themselves, as
93
+ sentence-transformers does. Note that special tokens written literally in a text (e.g. `<eos>`) are recognised as
94
+ special tokens, exactly as in Hugging Face `tokenizers`; strip them if that is not what you want.
95
+
96
+ ## Quick start — Swift
97
+
98
+ ```swift
99
+ // Package.swift: .package(path: "…/EmbeddingGemma-2-CoreAI-text-v1.0/swift"), product "EmbeddingGemma2CoreAI"
100
+ import EmbeddingGemma2CoreAI
101
+
102
+ let model = try await EmbeddingGemma2(bundle: URL(fileURLWithPath: "…/model"), compute: .neuralEngine)
103
+ try await model.prewarm() // optional, one call per bucket (~0.4 s warm)
104
+
105
+ let q = try await model.embed("When does the warehouse lease expire?", kind: "search_query") // [Float], 768
106
+ let d = try await model.embed("The lease runs until the last day of March.", kind: "document", dim: 256)
107
+ let score = cosineSimilarity(q, try await model.embed("The lease runs until the last day of March.", kind: "document"))
108
+
109
+ // lower level: your own ids -> raw output of one bucket
110
+ let ids = model.tokenize("some text", kind: "document") // <bos> + prefix + text + <eos>, truncated to 2048
111
+ let raw = try await model.embed(ids: ids) // runs main_<smallest bucket that fits>
112
+
113
+ // when the model is released (or at exit) you may delete the temp files this process created, see "Temp files"
114
+ EmbeddingGemma2.removeOwnMPSGraphTempFiles()
115
+ ```
116
+
117
+ `GemmaTokenizer` is public too (`encode`, `encodeWithSpecials`, `decode`): an exact re-implementation of the
118
+ Hugging Face `tokenizers` pipeline for this `tokenizer.json`, with no dependencies (see "Tokenizer" below).
119
+
120
+ Command line (`cd swift && swift build -c release`; the binary is `.build/release/eg2-cli` or
121
+ `.build/out/Products/Release/eg2-cli` depending on the toolchain):
122
+
123
+ ```
124
+ eg2-cli smoke ../model ../tests/smoke-reference.json --compute ane --cleanup-temp
125
+ eg2-cli embed ../model --kind search_query "When does the warehouse lease expire?"
126
+ eg2-cli tokcheck ../model ../tests/tokenizer-cases.json
127
+ eg2-cli latency ../model --compute ane --trials 50
128
+ eg2-cli prewarm ../model
129
+ ```
130
+
131
+ ## Quick start — Python
132
+
133
+ From the package folder:
134
+
135
+ ```
136
+ python3 -m venv .venv && .venv/bin/pip install -r python/requirements.txt
137
+ source .venv/bin/activate
138
+ cd python && python3 smoke_test.py --compute ane --cleanup-temp # first run: up to ~145 s to load, see below
139
+ ```
140
+
141
+ ```python
142
+ from embeddinggemma2_coreai import EmbeddingGemma2CoreAI, remove_own_mpsgraph_temp_files
143
+
144
+ m = EmbeddingGemma2CoreAI("../model", compute="ane") # ane | gpu | cpu | default
145
+ m.prewarm()
146
+ q = m.embed("When does the warehouse lease expire?", kind="search_query") # numpy float32 [768]
147
+ d = m.embed("The lease runs until the last day of March.", kind="document", dim=256)
148
+ del m
149
+ remove_own_mpsgraph_temp_files() # the Python runtime leaks ~358 MB per load, see below
150
+ ```
151
+
152
+ `python3 embeddinggemma2_coreai.py embed|smoke|tokcheck|latency|prewarm …` mirrors the Swift tool (run it with the
153
+ venv's Python, as above).
154
+
155
+ ## Accuracy (measured)
156
+
157
+ Reference: sentence-transformers 6.1 / transformers 5.19, fp32, CPU, original checkpoint. Test set: 357 fictional
158
+ texts (English and Italian documents and queries, code, mixed-language, Unicode/number/repetition stress cases, and
159
+ texts whose length lands exactly on each bucket boundary), 2 to 2048 tokens. The people, companies and events in
160
+ the test material are fictional; a few real place names appear in the multilingual stress texts.
161
+
162
+ **Parity** — cosine similarity between this port and the fp32 reference, full 768 dims (128-dim Matryoshka in
163
+ brackets):
164
+
165
+ | route | min | median | worst category |
166
+ |---|---|---|---|
167
+ | Neural Engine (`compute: .neuralEngine`), Swift and Python | 0.99992 (0.99995) | 0.99997 (0.99998) | 33-token bucket-boundary text |
168
+ | GPU (`compute: .gpu`) | 0.99998 (0.99999) | 0.999999 (1.000000) | code |
169
+ | CPU only (`compute: .cpu`), Swift and Python | 0.99916 (0.99914) | 0.99996 (0.99998) | 2047-token text (the CPU fp16 path is the least precise) |
170
+ | for scale: the original model itself in bf16 on the GPU (PyTorch MPS) | 0.99956 | 0.99996 | |
171
+
172
+ Per bucket on the Neural Engine the minimum is 0.99992–0.99996 and the median 0.99997–0.99998; no NaN/Inf on any
173
+ input, including fully padded and 1-token inputs. Texts longer than 2048 tokens are truncated by design and their
174
+ cosine to the full-length reference falls as expected (0.998 at 2100 tokens, 0.98 at 4500).
175
+
176
+ **Independent check.** A second, separate set of 100 new fictional texts (Italian, emoji, CRLF, Thai and other
177
+ scripts, NFD accents, code, one-word inputs, long texts, exact bucket boundaries, all 8 prompt kinds) against a
178
+ freshly downloaded fp32 sentence-transformers reference, Neural Engine route: minimum cosine 0.99985 (an emoji-heavy
179
+ text), median 0.99997 at 768 dims; 0.99993 / 0.999985 at 128 dims; Swift and Python identical (largest difference
180
+ 2.7e-7). **Expect a cosine ≥ 0.9998 to the fp32 model on the Neural Engine and GPU routes, ≥ 0.999 on the CPU route.**
181
+
182
+ **About float16.** The upstream model card says *"Do not use float16"*: the model's activations exceed the fp16
183
+ range and a naive fp16 run returns NaN or degraded vectors. This port runs in fp16 on purpose (the Neural Engine is
184
+ fp16-only) and avoids the overflow by dividing every RMSNorm row and the final L2 normalization by its max-abs value
185
+ before squaring (see NOTICE). The parity figures above are the evidence that this works; if you change the graph, keep
186
+ that rescaling.
187
+
188
+ **Retrieval** — 1,712 fictional passages, 53 queries (16 Italian), gold ranks compared rank-by-rank with the fp32
189
+ reference:
190
+
191
+ | dims | fp32 reference MRR | this port, Neural Engine, MRR | queries with the same first-relevant rank |
192
+ |---|---|---|---|
193
+ | 768 | 0.766 | 0.766 | 53 / 53 |
194
+ | 512 | 0.790 | 0.782 | 51 / 53 |
195
+ | 256 | 0.786 | 0.785 | 52 / 53 |
196
+ | 128 | 0.735 | 0.735 | 52 / 53 |
197
+
198
+ The flipped ranks are gold passages moving by one rank among near-tied neighbours (e.g. fp32 scores 0.82757 vs
199
+ 0.82755 at 512 dims) plus one query whose gold passage sits outside the top 10 in fp32 and at rank 10 here.
200
+ Italian MRR is identical at every size (0.831 / 0.830 / 0.834 / 0.716). The GPU route of this bundle was checked
201
+ for parity (table above) but not re-run on the retrieval benchmark.
202
+
203
+ **Smoke test** (`tests/smoke-reference.json`, 3 fictional sentences, threshold 0.999): Neural Engine cosine
204
+ 0.999948 / 0.999979 / 0.999976 (Swift and Python); Matryoshka 512 / 256 / 128 of the second sentence 0.999979 /
205
+ 0.999981 / 0.999989. GPU route 0.999999 / 0.999999 / 1.000000, CPU route 0.999970 / 0.999966 / 0.999970.
206
+
207
+ Caveats: these are one machine's numbers (M5, macOS 27.0 26A5406e). Core AI specializes the graph per machine
208
+ and OS build; the CPU-only route in particular depends on the OS's fp16 CPU kernels. Re-run `smoke` and, if you
209
+ care about the last digit, `tests/` on your target machines.
210
+
211
+ ## Performance (measured, Swift `eg2-cli latency`, 50 trials per bucket, M5)
212
+
213
+ Single runs on one machine; repeated runs differ by a few percent (e.g. 195–199 ms at 2048 tokens on the Neural
214
+ Engine).
215
+
216
+ | bucket (tokens) | Neural Engine p50 | CPU ms per call (NE) | GPU route p50 | CPU ms per call (GPU) |
217
+ |---|---|---|---|---|
218
+ | 32 | 4.8 ms | 0.24 | 7.6 ms | 10.3 |
219
+ | 64 | 5.3 ms | 0.24 | 7.9 ms | 10.3 |
220
+ | 128 | 6.6 ms | 0.26 | 10.3 ms | 11.1 |
221
+ | 256 | 11.6 ms | 0.27 | 12.2 ms | 11.7 |
222
+ | 512 | 27.8 ms | 0.26 | 21.9 ms | 14.8 |
223
+ | 1024 | 69 ms | 0.33 | 49 ms | 20.1 |
224
+ | 2048 | 195 ms | 0.39 | 142 ms | 30.6 |
225
+
226
+ The Python runner gives the same Neural Engine latencies within a few percent (CPU per call 0.4–0.9 ms). On the
227
+ Neural Engine route the GPU is used only for the int32 token gather: 0.38–0.54 ms of GPU time per call (32–1024
228
+ tokens), measured during validation with `task_power_info_v2` over 100-call batches. The GPU route is faster from 512
229
+ tokens up but burns 10–30 ms of CPU per call (MPSGraph encoding) and 1.5 GB of memory.
230
+ Throughput on a realistic corpus (1,712 passages, median 194 tokens, one text per call): about 50 passages/s,
231
+ 0.17 CPU-seconds per 100 passages.
232
+
233
+ Memory: process footprint ~230 MB after a warm load (peak ~700 MB during the first-ever load) plus ~600–770 MB of
234
+ Neural Engine footprint reported by the kernel; the GPU route peaks at ~1.5 GB.
235
+
236
+ **Load time** (`AIModel(contentsOf:)` + `loadFunction` for the 7 buckets):
237
+
238
+ | situation | time |
239
+ |---|---|
240
+ | warm (this executable has loaded this bundle before) | 0.4–1.5 s, plus 0.35 s for `prewarm()` |
241
+ | **first load by an executable that has not loaded this bundle before** (a new app, a new binary name, the Python interpreter, a new machine) | **~140 s** (139–144 s measured with three different executables) |
242
+ | first load by a new executable on a machine where another executable already compiled this exact bundle | can be shorter (~25–30 s observed), do not rely on it |
243
+ | GPU route, first load | ~9–22 s |
244
+
245
+ Recommendation: load and `prewarm()` in the background at app start (or in an installer / first-run step) and show
246
+ progress; never on the main thread and never lazily in front of the first query. The cache lives in
247
+ `~/Library/Caches/coreai-cache/<OS build>/<executable name>/` (about 1.4 GB per bundle per executable name, keyed
248
+ by the bundle's content hash) and survives reboots but not OS updates, a renamed executable or a re-exported
249
+ bundle. The tokenizer loads in 0.22 s (Swift) / 0.23 s (Python).
250
+
251
+ ## Temp files (please read)
252
+
253
+ Each Neural Engine load writes ~358 MB of compiled-graph copies to
254
+ `$TMPDIR/com.apple.MetalPerformanceShadersGraph/mpsgraph-<pid>-<date>-<n>/`. Measured on macOS 27.0:
255
+
256
+ - Swift (`CoreAI` framework): the folder is deleted again by the runtime when the model is released; our CLI, which
257
+ releases the model and then exits, leaves nothing behind in any of our runs. If your app exits or is killed while
258
+ the model is loaded, the folder may remain.
259
+ - Python (`coreai.runtime`): the folder is **never** deleted, not even after `del model` and a clean interpreter exit
260
+ (358 MB per load, verified for normal exit, `os._exit`, and `kill -9`). Over a day of development this fills a disk.
261
+
262
+ Both runners therefore ship a cleanup that removes only the folders belonging to the **current process** (its pid,
263
+ created after the process started): `EmbeddingGemma2.removeOwnMPSGraphTempFiles()` and
264
+ `remove_own_mpsgraph_temp_files()`, or `--cleanup-temp` on every CLI command. Call it after the model has been
265
+ released, or at exit. Folders of other processes are never touched; a leftover from a crashed run has to be removed
266
+ by hand (the folder name carries the pid and date).
267
+
268
+ ## Tokenizer
269
+
270
+ The Swift `GemmaTokenizer` re-implements the Hugging Face `tokenizers` pipeline described by `tokenizer.json`
271
+ (special-token extraction, `Replace " " → "▁"` normalizer, BPE with byte fallback, rank-ordered merges with
272
+ position tie-break). It works on Unicode scalars and exact UTF-8 bytes: no grapheme clusters, no
273
+ canonical-equivalence string comparisons, and `tokenizer.json` is read with an exact JSON reader because Foundation's
274
+ `JSONSerialization` strips a leading U+FEFF from string values (which silently corrupts three vocabulary entries).
275
+
276
+ Proof: `tests/tokenizer-cases.json` holds 3,758 fictional texts (608,225 ids) covering CRLF and mixed line endings,
277
+ NFD / NFKD / mixed-normalization accents, emoji ZWJ sequences, skin tones, flags and keycaps, Arabic and Persian
278
+ with diacritics, Hebrew, Thai, Hindi, Bengali, Tamil, Chinese (simplified and traditional), Japanese, Korean,
279
+ Georgian, Armenian, Amharic and 18 more languages, whitespace runs of 31–500 spaces / newlines / tabs / NBSP /
280
+ ideographic spaces, control characters and invisible code points, special-token look-alikes (`<bos>`, `<|tool>`,
281
+ `<start_of_turn>`, `< bos >`, `▁<bos>▁`, …), empty and 1-character inputs, random code points from 23 Unicode blocks
282
+ including unassigned, private-use and non-characters, and inputs of 5,000–80,000 characters. `eg2-cli tokcheck`
283
+ reports **3758 / 3758 identical** to `tokenizers` 0.23.3, in 0.15 s. The Python runner uses `tokenizers` directly.
284
+
285
+ Regenerate the fixture with `tests/make_tokenizer_cases.py` (needs `tokenizers`) and the reference vectors with
286
+ `tests/make_smoke_reference.py` (needs sentence-transformers and the original checkpoint).
287
+
288
+ ## Known issues and limitations
289
+
290
+ - **2048-token cap.** Longer inputs are truncated by the runners. Buckets of 4096/8192 can be exported
291
+ (`export.py --buckets …,4096,8192`) but were not validated here and would run on the GPU.
292
+ - **One text per call.** The functions are batch-1. Throughput for a corpus comes from calling them back to back;
293
+ running two or four calls concurrently did not increase throughput in our tests (the Neural Engine serializes).
294
+ - **Short inputs are not faster than ~4.8 ms.** There is a per-call floor in this Core AI build; 32- and 64-token
295
+ inputs cost the same.
296
+ - **CPU-only route is less precise** (cosine ≥ 0.9991) — use it only as a fallback.
297
+ - **Latency is not shape-stable across bucket sets.** Core AI specializes the whole function set together; adding
298
+ or removing buckets changed the 512-token latency by half (22 vs 34 ms) in our experiments. Measure the exact
299
+ bucket set you ship.
300
+ - **Specialization cache is per executable name.** Renaming your binary costs one first load of up to ~140 s.
301
+ - **Temp files** as described above.
302
+ - **fp16 output.** The embedding is fp16 (relative precision ~5e-4); convert to float32 before accumulating dot
303
+ products over many vectors, as the runners do.
304
+ - Validated on one machine and one OS build (macOS 27.0 26A5406e). Core AI is new in macOS 27 and its runtime
305
+ behaviour (first-load time, placement, temp files) may change between OS releases.
306
+
307
+ ## Reproducing the export
308
+
309
+ ```
310
+ # a separate environment with Python 3.12 (torch 2.9.0); exact versions in python/requirements.lock
311
+ python3.12 -m venv .venv-export && source .venv-export/bin/activate
312
+ pip install coreai-core==1.0.0b3 torch==2.9.0 coreai-torch==0.4.3 coreai-opt==0.3.0 torchao==0.17.0 \
313
+ safetensors numpy tokenizers huggingface-hub
314
+ # the original checkpoint: config.json, model.safetensors, tokenizer.json, tokenizer_config.json
315
+ hf download google/embeddinggemma-2 --local-dir ./embeddinggemma-2
316
+ cd export
317
+ python3 -W ignore export.py --model-dir ../embeddinggemma-2 --out ../model-rebuilt --name embeddinggemma-2-text \
318
+ --mode ane --buckets 32,64,128,256,512,1024,2048 --date 2026-10-12
319
+ ```
320
+
321
+ `eg2_text.py` is the re-authored encoder (plain PyTorch, `PRECISION_MODE = "ane"`: all fp16, max-abs-rescaled
322
+ norms, no divides in the model code; `GLOBAL_ATTN_MODE = "mqa4d"`). `export.py` does one dynamic-length `torch.export`, converts with
323
+ `coreai_torch.TorchConverter`, clones the entry point per bucket with `set_static_shape_config`, and writes the
324
+ bundle (~20 s). torchao must be 0.17.0 with torch 2.9 (0.18 breaks `import coreai_opt`).
325
+
326
+ The rebuilt bundle is **functionally identical, not byte-identical**: its smoke-test cosines match the shipped bundle
327
+ to all printed digits (0.999948 / 0.999979 / 0.999976), but the converter names internal graph resources at random,
328
+ so `main.mlirb` and `main.hash` differ by a few KB, and the runtime writes `metadata.json` with the export time and
329
+ its own key order. `config.json`, `MODEL-ATTRIBUTION.json` (with `--date 2026-10-12`) and the tokenizer files come out
330
+ byte-identical, so those four entries of `CHECKSUMS.sha256` match. Verify a rebuild with `smoke`, not with
331
+ checksums. A rebuilt bundle has a new content hash, so its first load takes the full ~140 s again.
332
+
333
+ To check Neural Engine eligibility without running: `xcrun coreai-build compile model/embeddinggemma-2-text.aimodel
334
+ --output ./compiled --preferred-compute neural-engine --architecture h17g 2>&1 | grep -c ane_validation_message`
335
+ prints 0 for this bundle (no op rejected; the output folder is about 700 MB, delete it afterwards).
336
+
337
+ ## Credits and license
338
+
339
+ - Model: EmbeddingGemma 2 by the Gemma Team, Google DeepMind — Apache-2.0. Deployments must adhere to the
340
+ [Gemma Prohibited Use Policy](https://ai.google.dev/gemma/prohibited_use_policy).
341
+ - Architecture reference: Hugging Face transformers (Apache-2.0).
342
+ - The max-abs RMSNorm rescaling idea comes from the FluidInference Core ML port of this model (Apache-2.0); no code
343
+ was copied.
344
+ - Port, exporter, runners, tokenizer and tests: Copyright 2026 Everpaper, Apache-2.0.
345
+ - Gemma and EmbeddingGemma are trademarks of Google LLC; Apple, Core AI, Apple silicon and Neural Engine are
346
+ trademarks of Apple Inc. This port is not affiliated with, sponsored by or endorsed by Google or Apple.
export/eg2_text.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
2
+ #
3
+ # Re-authoring of the EmbeddingGemma 2 text encoder architecture as described by
4
+ # modeling_embedding_gemma2.py / configuration_embedding_gemma2.py in Hugging Face transformers 5.19
5
+ # (Copyright 2026 The HuggingFace Team, Apache-2.0). Model weights: Google DeepMind (Gemma Team), Apache-2.0,
6
+ # https://huggingface.co/google/embeddinggemma-2 — see ../NOTICE for the full attribution and modification list.
7
+ """EmbeddingGemma 2 text encoder re-authored for Core AI export (standard [B, S, D] layout, batch 1).
8
+
9
+ Everything the sentence-transformers pipeline does is inside one graph:
10
+ input_ids [1, S] int32 + attention_mask [1, S] (1 = token, 0 = right padding)
11
+ -> scaled token embedding (sqrt(512) folded into the fp16 table)
12
+ -> projection-only per-layer embeddings (PLE)
13
+ -> 24 bidirectional encoder layers (5 sliding-window-512 : 1 full, GQA 4 heads / 2 kv (sliding, hd 256) or 1 kv (full, hd 512))
14
+ -> final RMSNorm -> 512->768 projection -> masked mean pooling -> L2 normalization
15
+ -> embedding [1, 768]
16
+
17
+ Precision modes (PRECISION_MODE, set before construction):
18
+ "ane" every op fp16 (Neural Engine eligible). RMSNorm and the final L2 norm divide the row by its max-abs value
19
+ before squaring (the model's activations reach ~2000, whose square overflows fp16; this rescaling idea comes
20
+ from the README of the FluidInference Core ML port of this model and was re-derived here).
21
+ Softmax in fp16 (scores are bounded because q/k are RMS-normalized with scale 1.0).
22
+ "gpu" fp16 weights/matmuls, fp32 islands for every RMSNorm, the softmax, the pooling and the L2 norm
23
+ (the HF reference computes the norms and the softmax in fp32).
24
+ "fp32" the whole model in fp32 (correctness gate for the re-authoring itself; never exported).
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import math
30
+ from pathlib import Path
31
+
32
+ import torch
33
+ import torch.nn as nn
34
+ import torch.nn.functional as F
35
+
36
+ PRECISION_MODE = "ane" # "ane" | "gpu" | "fp32"
37
+ GQA_MODE = "grouped5d" # "expand" (HF repeat_kv: expand+reshape) | "grouped5d" (no repeat, 5-D batched matmul) | "repeat_interleave"
38
+ # Global (full-attention, MQA: 1 kv head, head_dim 512) layers 5/11/17/23. "grouped5d" = the GQA_MODE path.
39
+ # "mqa4d" (the released bundle) = plain 4-D matmul with k/v broadcast over the heads. With grouped5d the Neural
40
+ # Engine compiler rejects the MQA matmul ("Unsupported mps.matmul op for this ANE architecture"), every global layer
41
+ # becomes a GPU region, and MPSGraph then places the short buckets entirely on the GPU; mqa4d keeps one GPU region
42
+ # (the token gather) and everything else on the ANE. Mathematically identical (fp32 max|diff| 3e-7).
43
+ GLOBAL_ATTN_MODE = __import__("os").environ.get("EG2_GLOBAL_ATTN", "mqa4d")
44
+ # repeat_interleave made the Neural Engine compiler's fused SDPA fail on the MQA layers (1 kv head, 4 q heads):
45
+ # "'mps_spi.sdpa' op failed: grouping for the value tensor does not match the one available on the key tensor".
46
+ MASK_VALUE = -10000.0 # finite (ANE softmax mishandles -inf); fp16-exact
47
+ NORM_FLOOR = 1e-3 # floor for the row max-abs in "ane" RMSNorm (sqrt(eps) = 1e-3 for eps 1e-6)
48
+
49
+
50
+ class Cfg:
51
+ def __init__(self, model_dir: str):
52
+ c = json.loads((Path(model_dir) / "config.json").read_text())["text_config"]
53
+ self.hidden = c["hidden_size"] # 512
54
+ self.inter = c["intermediate_size"] # 2048
55
+ self.n_layers = c["num_hidden_layers"] # 24
56
+ self.n_heads = c["num_attention_heads"] # 4
57
+ self.layer_types = c["layer_types"]
58
+ self.sliding_window = c["sliding_window"] # 512 (inclusive distance)
59
+ self.eps = c["rms_norm_eps"]
60
+ self.vocab = c["vocab_size"]
61
+ self.embed_dim = c["embedding_dim"] # 768
62
+ self.ple_dim = c["hidden_size_per_layer_input"] # 512
63
+ self.act = c["hidden_activation"]
64
+ assert self.act == "gelu_pytorch_tanh", self.act
65
+ self.rope_theta = {k: v["rope_theta"] for k, v in c["rope_parameters"].items()}
66
+ per = c.get("per_layer_config", {})
67
+ self.head_dim = [per.get(f"{i:02d}", {}).get("head_dim", c["head_dim"]) for i in range(self.n_layers)]
68
+ self.kv_heads = [per.get(f"{i:02d}", {}).get("num_key_value_heads", c["num_key_value_heads"])
69
+ for i in range(self.n_layers)]
70
+
71
+
72
+ def _f16(x: float) -> torch.Tensor:
73
+ return torch.tensor(x, dtype=torch.float16)
74
+
75
+
76
+ def _recip(m: torch.Tensor) -> torch.Tensor:
77
+ """1/m for m > 0 without a divide: the Neural Engine compiler rejects `mps.divide` (falls back to the GPU);
78
+ rsqrt is native. rsqrt(m)*rsqrt(m) stays in fp16 range for m in [6e-5, 65504]."""
79
+ r = torch.rsqrt(m)
80
+ return r * r
81
+
82
+
83
+ class RMSNorm(nn.Module):
84
+ """Gemma RMSNorm: x * rsqrt(mean(x^2) + eps) * w (w stored as the raw weight, no +1)."""
85
+
86
+ def __init__(self, dim: int, eps: float, with_scale: bool = True):
87
+ super().__init__()
88
+ self.with_scale = with_scale
89
+ if with_scale:
90
+ self.weight = nn.Parameter(torch.ones(dim), requires_grad=False)
91
+ self.eps = eps
92
+ self.register_buffer("eps16", _f16(eps), persistent=False)
93
+ self.register_buffer("sqrt_eps16", _f16(math.sqrt(eps)), persistent=False)
94
+ self.register_buffer("floor16", _f16(NORM_FLOOR), persistent=False)
95
+
96
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
97
+ if PRECISION_MODE == "ane":
98
+ inv_m = _recip(torch.maximum(x.abs().amax(-1, keepdim=True), self.floor16))
99
+ u = x * inv_m
100
+ r = self.sqrt_eps16 * inv_m
101
+ inv = torch.rsqrt((u * u).mean(-1, keepdim=True) + r * r)
102
+ y = u * inv
103
+ return y * self.weight if self.with_scale else y
104
+ xf = x.float()
105
+ y = xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + self.eps)
106
+ if self.with_scale:
107
+ y = y * self.weight.float()
108
+ return y.to(x.dtype)
109
+
110
+
111
+ def rotate_half(x: torch.Tensor) -> torch.Tensor:
112
+ h = x.shape[-1] // 2
113
+ return torch.cat((-x[..., h:], x[..., :h]), dim=-1)
114
+
115
+
116
+ class Attention(nn.Module):
117
+ def __init__(self, cfg: Cfg, i: int):
118
+ super().__init__()
119
+ self.hd = cfg.head_dim[i]
120
+ self.n_heads = cfg.n_heads
121
+ self.n_kv = cfg.kv_heads[i]
122
+ self.groups = self.n_heads // self.n_kv
123
+ self.q_proj = nn.Linear(cfg.hidden, cfg.n_heads * self.hd, bias=False)
124
+ self.k_proj = nn.Linear(cfg.hidden, self.n_kv * self.hd, bias=False)
125
+ self.v_proj = nn.Linear(cfg.hidden, self.n_kv * self.hd, bias=False)
126
+ self.o_proj = nn.Linear(cfg.n_heads * self.hd, cfg.hidden, bias=False)
127
+ self.q_norm = RMSNorm(self.hd, cfg.eps)
128
+ self.k_norm = RMSNorm(self.hd, cfg.eps)
129
+ self.v_norm = RMSNorm(self.hd, cfg.eps, with_scale=False)
130
+
131
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, bias: torch.Tensor) -> torch.Tensor:
132
+ B, S, _ = x.shape
133
+ q = self.q_proj(x).view(B, S, self.n_heads, self.hd).transpose(1, 2)
134
+ k = self.k_proj(x).view(B, S, self.n_kv, self.hd).transpose(1, 2)
135
+ v = self.v_proj(x).view(B, S, self.n_kv, self.hd).transpose(1, 2)
136
+ q, k, v = self.q_norm(q), self.k_norm(k), self.v_norm(v)
137
+ q = q * cos + rotate_half(q) * sin
138
+ k = k * cos + rotate_half(k) * sin
139
+ if self.n_kv == 1 and GLOBAL_ATTN_MODE == "mqa4d":
140
+ # MQA (global layers, 1 kv head): plain 4-D matmul, k/v broadcast over the 4 query heads, no repeat
141
+ scores = torch.matmul(q, k.transpose(-1, -2)) + bias
142
+ p = torch.softmax(scores, dim=-1) if PRECISION_MODE == "ane" else torch.softmax(scores.float(), dim=-1).to(q.dtype)
143
+ o = torch.matmul(p, v)
144
+ elif GQA_MODE == "grouped5d" and self.groups > 1:
145
+ qg = q.view(B, self.n_kv, self.groups, S, self.hd)
146
+ scores = torch.matmul(qg, k.transpose(-1, -2)[:, :, None]) + bias[:, None] # [B, n_kv, groups, S, S]
147
+ p = torch.softmax(scores, dim=-1) if PRECISION_MODE == "ane" else torch.softmax(scores.float(), dim=-1).to(q.dtype)
148
+ o = torch.matmul(p, v[:, :, None]).view(B, self.n_heads, S, self.hd)
149
+ else:
150
+ if self.groups > 1:
151
+ if GQA_MODE == "repeat_interleave":
152
+ k = k.repeat_interleave(self.groups, dim=1)
153
+ v = v.repeat_interleave(self.groups, dim=1)
154
+ else:
155
+ k = k[:, :, None].expand(B, self.n_kv, self.groups, S, self.hd).reshape(B, self.n_heads, S, self.hd)
156
+ v = v[:, :, None].expand(B, self.n_kv, self.groups, S, self.hd).reshape(B, self.n_heads, S, self.hd)
157
+ scores = torch.matmul(q, k.transpose(-1, -2)) + bias # scale 1.0 (the model's q/k norms carry it)
158
+ p = torch.softmax(scores, dim=-1) if PRECISION_MODE == "ane" else torch.softmax(scores.float(), dim=-1).to(q.dtype)
159
+ o = torch.matmul(p, v)
160
+ o = o.transpose(1, 2).reshape(B, S, self.n_heads * self.hd)
161
+ return self.o_proj(o)
162
+
163
+
164
+ class MLP(nn.Module):
165
+ def __init__(self, cfg: Cfg):
166
+ super().__init__()
167
+ self.gate_proj = nn.Linear(cfg.hidden, cfg.inter, bias=False)
168
+ self.up_proj = nn.Linear(cfg.hidden, cfg.inter, bias=False)
169
+ self.down_proj = nn.Linear(cfg.inter, cfg.hidden, bias=False)
170
+
171
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
172
+ return self.down_proj(F.gelu(self.gate_proj(x), approximate="tanh") * self.up_proj(x))
173
+
174
+
175
+ class PLEBlock(nn.Module):
176
+ def __init__(self, cfg: Cfg):
177
+ super().__init__()
178
+ self.per_layer_input_gate = nn.Linear(cfg.hidden, cfg.ple_dim, bias=False)
179
+ self.per_layer_projection = nn.Linear(cfg.ple_dim, cfg.hidden, bias=False)
180
+ self.post_per_layer_input_norm = RMSNorm(cfg.hidden, cfg.eps)
181
+
182
+ def forward(self, x: torch.Tensor, ple: torch.Tensor) -> torch.Tensor:
183
+ h = F.gelu(self.per_layer_input_gate(x), approximate="tanh") * ple
184
+ return x + self.post_per_layer_input_norm(self.per_layer_projection(h))
185
+
186
+
187
+ class Layer(nn.Module):
188
+ def __init__(self, cfg: Cfg, i: int):
189
+ super().__init__()
190
+ self.self_attn = Attention(cfg, i)
191
+ self.mlp = MLP(cfg)
192
+ self.input_layernorm = RMSNorm(cfg.hidden, cfg.eps)
193
+ self.post_attention_layernorm = RMSNorm(cfg.hidden, cfg.eps)
194
+ self.pre_feedforward_layernorm = RMSNorm(cfg.hidden, cfg.eps)
195
+ self.post_feedforward_layernorm = RMSNorm(cfg.hidden, cfg.eps)
196
+ self.ple_block = PLEBlock(cfg)
197
+ self.register_buffer("layer_scalar", torch.ones(1))
198
+
199
+ def forward(self, x, ple, cos, sin, bias):
200
+ x = x + self.post_attention_layernorm(self.self_attn(self.input_layernorm(x), cos, sin, bias))
201
+ x = x + self.post_feedforward_layernorm(self.mlp(self.pre_feedforward_layernorm(x)))
202
+ x = self.ple_block(x, ple)
203
+ return x * self.layer_scalar
204
+
205
+
206
+ class EmbeddingGemma2Text(nn.Module):
207
+ def __init__(self, cfg: Cfg, max_seq: int = 8192):
208
+ super().__init__()
209
+ self.cfg = cfg
210
+ self.max_seq = max_seq
211
+ self.embed_tokens = nn.Embedding(cfg.vocab, cfg.hidden) # sqrt(hidden) folded in at load
212
+ self.per_layer_model_projection = nn.Linear(cfg.hidden, cfg.n_layers * cfg.ple_dim, bias=False) # scale folded
213
+ self.per_layer_projection_norm = RMSNorm(cfg.ple_dim, cfg.eps)
214
+ self.layers = nn.ModuleList([Layer(cfg, i) for i in range(cfg.n_layers)])
215
+ self.norm = RMSNorm(cfg.hidden, cfg.eps)
216
+ self.embedding_projection = nn.Linear(cfg.hidden, cfg.embed_dim, bias=False)
217
+ # RoPE tables [max_seq, hd] per (layer type); head dims differ: sliding 256 @ theta 1e4, full 512 @ theta 1e6
218
+ for lt in ("sliding_attention", "full_attention"):
219
+ hd = cfg.head_dim[cfg.layer_types.index(lt)]
220
+ inv = 1.0 / (cfg.rope_theta[lt] ** (torch.arange(0, hd, 2, dtype=torch.float32) / hd))
221
+ fr = torch.arange(max_seq, dtype=torch.float32)[:, None] * inv[None]
222
+ emb = torch.cat((fr, fr), dim=-1)
223
+ self.register_buffer(f"cos_{lt}", emb.cos(), persistent=False)
224
+ self.register_buffer(f"sin_{lt}", emb.sin(), persistent=False)
225
+ self.register_buffer("mask_value", _f16(MASK_VALUE), persistent=False)
226
+ self.register_buffer("one", _f16(1.0), persistent=False)
227
+ self.register_buffer("zero", _f16(0.0), persistent=False)
228
+ self.register_buffer("floor16", _f16(NORM_FLOOR), persistent=False)
229
+ self.register_buffer("window", torch.tensor(cfg.sliding_window, dtype=torch.int32), persistent=False)
230
+
231
+ def _biases(self, attention_mask: torch.Tensor, S: int):
232
+ pad = (self.one - attention_mask) * self.mask_value # [1, S] 0 for tokens, -1e4 for padding keys
233
+ full = pad[:, None, None, :] # [1, 1, 1, S] broadcast over heads and queries
234
+ pos = torch.arange(S, device=attention_mask.device, dtype=torch.int32)
235
+ dist = (pos[:, None] - pos[None, :]).abs()
236
+ band = torch.where(dist <= self.window, self.zero, self.mask_value) # [S, S]
237
+ sliding = full + band[None, None]
238
+ return full, sliding
239
+
240
+ def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
241
+ S = input_ids.shape[1]
242
+ x = self.embed_tokens(input_ids) # [1, S, 512], already scaled
243
+ ple = self.per_layer_projection_norm(
244
+ self.per_layer_model_projection(x).view(1, S, self.cfg.n_layers, self.cfg.ple_dim))
245
+ full, sliding = self._biases(attention_mask, S)
246
+ cs = {}
247
+ for lt in ("sliding_attention", "full_attention"):
248
+ cs[lt] = (getattr(self, f"cos_{lt}")[:S].to(x.dtype)[None, None], getattr(self, f"sin_{lt}")[:S].to(x.dtype)[None, None])
249
+ for i, layer in enumerate(self.layers):
250
+ lt = self.cfg.layer_types[i]
251
+ x = layer(x, ple[:, :, i, :], cs[lt][0], cs[lt][1], sliding if lt == "sliding_attention" else full)
252
+ x = self.embedding_projection(self.norm(x)) # [1, S, 768]
253
+ # masked mean pooling: weights sum to 1, so no large partial sums in fp16
254
+ if PRECISION_MODE == "ane":
255
+ w = attention_mask * _recip(attention_mask.sum(-1, keepdim=True)) # [1, S] (count exact in fp16 up to 2048)
256
+ pooled = torch.matmul(w[:, None, :], x)[:, 0, :] # [1, 768]
257
+ u = pooled * _recip(torch.maximum(pooled.abs().amax(-1, keepdim=True), self.floor16))
258
+ return u * torch.rsqrt((u * u).sum(-1, keepdim=True))
259
+ xf = x.float()
260
+ wf = attention_mask.float()
261
+ pooled = (xf * wf[:, :, None]).sum(1) / wf.sum(-1, keepdim=True)
262
+ return (pooled / pooled.norm(dim=-1, keepdim=True)).to(x.dtype)
263
+
264
+
265
+ def load_model(model_dir: str, dtype: torch.dtype, max_seq: int = 8192) -> EmbeddingGemma2Text:
266
+ from safetensors import safe_open
267
+
268
+ cfg = Cfg(model_dir)
269
+ m = EmbeddingGemma2Text(cfg, max_seq)
270
+ sd = {}
271
+ with safe_open(str(Path(model_dir) / "model.safetensors"), "pt") as f:
272
+ for k in f.keys():
273
+ if not k.startswith("language_model."):
274
+ continue
275
+ t = f.get_tensor(k).float()
276
+ k = k[len("language_model."):]
277
+ if k == "embed_tokens.weight":
278
+ t = t * math.sqrt(cfg.hidden)
279
+ elif k == "ple.per_layer_model_projection.weight":
280
+ k = "per_layer_model_projection.weight"
281
+ t = t * cfg.hidden ** -0.5
282
+ elif k == "ple.per_layer_projection_norm.weight":
283
+ k = "per_layer_projection_norm.weight"
284
+ sd[k] = t.to(dtype)
285
+ missing, unexpected = m.load_state_dict(sd, strict=False)
286
+ missing = [k for k in missing if not any(s in k for s in ("cos_", "sin_", "eps16", "sqrt_eps16", "floor16", "mask_value", "one", "zero", "window"))]
287
+ assert not missing and not unexpected, (missing, unexpected)
288
+ return m.to(dtype).eval()
export/export.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
2
+ """Export the EmbeddingGemma 2 text encoder to a Core AI `.aimodel` with one static entrypoint per sequence bucket.
3
+
4
+ # 1. download the original checkpoint: hf download google/embeddinggemma-2 --local-dir ./embeddinggemma-2
5
+ # 2. python3 -W ignore export.py --model-dir ./embeddinggemma-2 --out <bundle folder> --name embeddinggemma-2-text \
6
+ # --mode ane --buckets 32,64,128,256,512,1024,2048 (the released bundle; ~20 s on an M-series Mac)
7
+
8
+ The result is functionally identical to the released bundle (same outputs), not byte-identical: the converter gives
9
+ internal graph resources random names, so main.mlirb / main.hash differ, and the runtime writes metadata.json with the
10
+ export time and its own key order. config.json, MODEL-ATTRIBUTION.json (with --date 2026-10-12) and the tokenizer
11
+ files come out byte-identical to the released ones.
12
+
13
+ One dynamic-S torch.export -> TorchConverter -> AIProgram.set_static_shape_config("main", {...}) which replaces
14
+ `main` by `main_<S>` clones (static shapes are what keeps the graph on the Neural Engine).
15
+
16
+ Bundle layout:
17
+ <out>/<name>.aimodel functions main_<S>: inputs input_ids [1,S] int32, attention_mask [1,S] fp16 -> embedding [1,768] fp16
18
+ <out>/tokenizer/ tokenizer.json + tokenizer_config.json (unchanged Gemma tokenizer)
19
+ <out>/config.json buckets, prompts, special tokens, matryoshka dims, precision mode
20
+ <out>/MODEL-ATTRIBUTION.json base model, checkpoint sha256, exporter versions, modification list
21
+
22
+ Requirements (see ../python/requirements.lock): Python 3.12, torch 2.9.0, coreai-torch 0.4.3, coreai-opt 0.3.0,
23
+ torchao 0.17.0, coreai-core 1.0.0b3, safetensors.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import hashlib
29
+ import json
30
+ import os
31
+ import shutil
32
+ import subprocess
33
+ import sys
34
+ import time
35
+ from pathlib import Path
36
+
37
+ import torch
38
+
39
+ LAB = Path(__file__).resolve().parent
40
+ sys.path.insert(0, str(LAB))
41
+ import eg2_text # noqa: E402
42
+
43
+ T0 = time.time()
44
+ PROMPTS = {
45
+ "document": "title: none | text: ",
46
+ "search_query": "task: search result | query: ",
47
+ "question_answering": "task: question answering | query: ",
48
+ "fact_checking": "task: fact checking | query: ",
49
+ "code_retrieval": "task: code retrieval | query: ",
50
+ "classification": "task: classification | query: ",
51
+ "clustering": "task: clustering | query: ",
52
+ "sentence_similarity": "task: sentence similarity | query: ",
53
+ }
54
+
55
+
56
+ def log(msg: str) -> None:
57
+ print(f"[{time.time() - T0:7.1f}s] {msg}", flush=True)
58
+
59
+
60
+ def sha256_file(p: Path) -> str:
61
+ h = hashlib.sha256()
62
+ with open(p, "rb") as f:
63
+ for chunk in iter(lambda: f.read(1 << 24), b""):
64
+ h.update(chunk)
65
+ return h.hexdigest()
66
+
67
+
68
+ def dir_bytes(p: Path) -> int:
69
+ return sum(f.stat().st_size for f in p.rglob("*") if f.is_file()) if p.is_dir() else p.stat().st_size
70
+
71
+
72
+ def main() -> None:
73
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
74
+ ap.add_argument("--model-dir", required=True, help="folder with the original google/embeddinggemma-2 checkpoint "
75
+ "(config.json, model.safetensors, tokenizer.json, tokenizer_config.json)")
76
+ ap.add_argument("--hf-id", default="google/embeddinggemma-2")
77
+ ap.add_argument("--out", required=True)
78
+ ap.add_argument("--name", default=None)
79
+ ap.add_argument("--mode", default="ane", choices=["ane", "gpu"])
80
+ ap.add_argument("--buckets", default="32,64,128,256,512,1024,2048")
81
+ ap.add_argument("--num-layers", type=int, default=None, help="smoke export with the first N layers")
82
+ ap.add_argument("--debug-info", action="store_true")
83
+ ap.add_argument("--no-static", action="store_true", help="keep the single dynamic-S `main` (GPU only)")
84
+ ap.add_argument("--date", default=None, help="port date written to MODEL-ATTRIBUTION.json (default: today)")
85
+ args = ap.parse_args()
86
+ buckets = sorted(int(b) for b in args.buckets.split(","))
87
+ max_seq = max(buckets)
88
+ out = Path(args.out)
89
+ name = args.name or out.name
90
+ eg2_text.PRECISION_MODE = args.mode
91
+ log(f"args {vars(args)}")
92
+
93
+ model = eg2_text.load_model(args.model_dir, torch.float16, max_seq=max_seq)
94
+ if args.num_layers:
95
+ model.layers = model.layers[: args.num_layers]
96
+ model.cfg.layer_types = model.cfg.layer_types[: args.num_layers]
97
+ log(f"model loaded: {sum(p.numel() for p in model.parameters()) / 1e6:.1f} M params, mode={args.mode}")
98
+
99
+ import coreai_torch
100
+ from torch.export import Dim
101
+
102
+ S0 = buckets[min(1, len(buckets) - 1)]
103
+ ref = {"input_ids": torch.zeros(1, S0, dtype=torch.int32),
104
+ "attention_mask": torch.ones(1, S0, dtype=torch.float16)}
105
+ ref["input_ids"][0, :8] = torch.tensor([2, 8071, 236787, 3927, 1354, 1109, 7609, 1], dtype=torch.int32)
106
+ ref["attention_mask"][0, 8:] = 0
107
+ S = Dim("S", min=min(8, buckets[0]), max=max_seq)
108
+ dyn = {"input_ids": {1: S}, "attention_mask": {1: S}}
109
+ with torch.no_grad():
110
+ ep = torch.export.export(model, args=(), kwargs=ref, dynamic_shapes=dyn)
111
+ log("torch.export done")
112
+ ep = ep.run_decompositions(coreai_torch.get_decomp_table())
113
+ log("decomposed")
114
+ mode = coreai_torch.TorchConverter.Mode.DEBUG if args.debug_info else coreai_torch.TorchConverter.Mode.RELEASE
115
+ conv = coreai_torch.TorchConverter(mode=mode)
116
+ conv.add_exported_program(ep, input_names=["input_ids", "attention_mask"], output_names=["embedding"],
117
+ entrypoint_name="main")
118
+ prog = conv.to_coreai()
119
+ log("converted to AIProgram")
120
+ functions = ["main"]
121
+ if not args.no_static:
122
+ cfg = {f'"{b}"': {"input_ids": (1, b), "attention_mask": (1, b)} for b in buckets}
123
+ prog.set_static_shape_config("main", cfg)
124
+ functions = [f"main_{b}" for b in buckets]
125
+ log(f"static shape specializations: {functions}")
126
+
127
+ from coreai.runtime import AIModelAssetMetadata
128
+
129
+ out.mkdir(parents=True, exist_ok=True)
130
+ aimodel = out / f"{name}.aimodel"
131
+ shutil.rmtree(aimodel, ignore_errors=True)
132
+ meta = AIModelAssetMetadata()
133
+ meta.author = "Gemma Team (Google); Core AI port"
134
+ meta.license = "Apache-2.0"
135
+ meta.model_description = (f"EmbeddingGemma 2 text encoder ({args.hf_id}) with mean pooling, 768-d projection and L2 "
136
+ f"normalization in-graph; precision mode {args.mode}; sequence buckets {buckets}.")
137
+ meta.creation_date = int(time.time())
138
+ prog.save_asset(aimodel, meta)
139
+ log(f"saved {aimodel} ({dir_bytes(aimodel) / 2**20:.1f} MiB)")
140
+
141
+ # ---- bundle ---------------------------------------------------------------------
142
+ tok_dir = out / "tokenizer"
143
+ tok_dir.mkdir(exist_ok=True)
144
+ for f in ("tokenizer.json", "tokenizer_config.json"):
145
+ shutil.copy(Path(args.model_dir) / f, tok_dir / f)
146
+ cfg_json = {
147
+ "base_model": args.hf_id, "format": "coreai", "asset": aimodel.name, "functions": functions,
148
+ "precision_mode": args.mode, "buckets": buckets, "max_tokens": max_seq,
149
+ "inputs": {"input_ids": "[1, S] int32 (bos=2 ... eos=1, right-padded with pad=0)",
150
+ "attention_mask": "[1, S] float16 (1 = token, 0 = padding)"},
151
+ "output": {"embedding": "[1, 768] float16, L2-normalized; Matryoshka: keep the first 512/256/128 and re-normalize"},
152
+ "hidden_size": 512, "embedding_dim": 768, "matryoshka_dims": [768, 512, 256, 128],
153
+ "bos_token_id": 2, "eos_token_id": 1, "pad_token_id": 0, "prompts": PROMPTS,
154
+ "tokenizer": "tokenizer/tokenizer.json (unchanged Gemma tokenizer; the runner adds <bos> and <eos>)",
155
+ }
156
+ (out / "config.json").write_text(json.dumps(cfg_json, indent=2) + "\n")
157
+ import coreai_torch as ct
158
+ from importlib.metadata import version as pkg_version
159
+
160
+ rescale = ("fp16 everywhere (Neural Engine): every RMSNorm and the final L2 normalization divide the row by its "
161
+ "max-abs value before squaring, so activations (~2000) never overflow fp16; in the model code every "
162
+ "division is expressed as rsqrt(x)*rsqrt(x) (the Neural Engine compiler rejects divide)."
163
+ if args.mode == "ane" else "fp16 weights and matmuls; RMSNorm, softmax, pooling and L2 normalization in fp32.")
164
+ attribution = {
165
+ "name": "EmbeddingGemma-2-CoreAI-text",
166
+ "version": "1.0",
167
+ "base_model": args.hf_id,
168
+ "base_model_author": "Gemma Team, Google DeepMind",
169
+ "base_model_source": f"https://huggingface.co/{args.hf_id}",
170
+ "base_model_license": "Apache-2.0",
171
+ "base_model_terms": "Deployments must adhere to the Gemma Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy",
172
+ "base_model_file_sha256": {"model.safetensors": sha256_file(Path(args.model_dir) / "model.safetensors")},
173
+ "port": {
174
+ "copyright": "2026 Everpaper",
175
+ "license": "Apache-2.0 (see LICENSE); attribution and modification notice in NOTICE",
176
+ "date": args.date or time.strftime("%Y-%m-%d"),
177
+ "format": "Apple Core AI .aimodel (macOS 27), fp16, static sequence buckets",
178
+ "exporter": {
179
+ "coreai_torch": ct.__version__,
180
+ "coreai_core": pkg_version("coreai-core"),
181
+ "torch": torch.__version__.split("+")[0],
182
+ "recipe": f"export/export.py --mode {args.mode} --buckets {','.join(map(str, buckets))}",
183
+ "global_attention": eg2_text.GLOBAL_ATTN_MODE,
184
+ "gqa": eg2_text.GQA_MODE,
185
+ },
186
+ },
187
+ "modifications": [
188
+ "Text encoder only: the vision and audio encoders of the multimodal checkpoint are not included.",
189
+ "Architecture re-authored in plain PyTorch for Core AI export; weights are the original ones, rounded from bf16 to fp16.",
190
+ "The sqrt(512) token-embedding scale and the 512^-0.5 per-layer-embedding scale are folded into the tables.",
191
+ "The four global (full-attention, single-kv-head) layers compute attention as a plain 4-D matmul with k/v "
192
+ "broadcast over the heads; the sliding-window layers use a 5-D grouped matmul. Mathematically identical to "
193
+ "the reference (fp32 max abs diff 3e-7).",
194
+ rescale,
195
+ "Bidirectional attention masks (padding + sliding window 512) are built inside the graph from attention_mask; "
196
+ "the additive mask value is -10000 (finite).",
197
+ "Masked mean pooling, the 512->768 embedding projection and the L2 normalization run inside the graph; the "
198
+ "output is the final sentence embedding.",
199
+ f"Fixed sequence buckets {', '.join(map(str, buckets))} as functions main_<S>; input longer than {max_seq} "
200
+ "tokens must be truncated or chunked by the caller (the original model accepts 8192).",
201
+ ],
202
+ }
203
+ if args.num_layers:
204
+ attribution["port"]["exporter"]["num_layers"] = args.num_layers
205
+ (out / "MODEL-ATTRIBUTION.json").write_text(json.dumps(attribution, indent=2) + "\n")
206
+ log(f"bundle written: {out} ({dir_bytes(out) / 2**20:.1f} MiB)")
207
+ try:
208
+ print(subprocess.run(["xcrun", "coreai-build", "inspect", str(aimodel), "--no-metadata"], capture_output=True, text=True, timeout=120).stdout[:3000])
209
+ except Exception as e: # noqa: BLE001
210
+ log(f"inspect failed: {e}")
211
+ log("DONE")
212
+
213
+
214
+ if __name__ == "__main__":
215
+ main()
model/MODEL-ATTRIBUTION.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "EmbeddingGemma-2-CoreAI-text",
3
+ "version": "1.0",
4
+ "base_model": "google/embeddinggemma-2",
5
+ "base_model_author": "Gemma Team, Google DeepMind",
6
+ "base_model_source": "https://huggingface.co/google/embeddinggemma-2",
7
+ "base_model_license": "Apache-2.0",
8
+ "base_model_terms": "Deployments must adhere to the Gemma Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy",
9
+ "base_model_file_sha256": {
10
+ "model.safetensors": "197a32965d4b1105faf060417baa899e193fb73cd401f42ec9295234d5553d79"
11
+ },
12
+ "port": {
13
+ "copyright": "2026 Everpaper",
14
+ "license": "Apache-2.0 (see LICENSE); attribution and modification notice in NOTICE",
15
+ "date": "2026-10-12",
16
+ "format": "Apple Core AI .aimodel (macOS 27), fp16, static sequence buckets",
17
+ "exporter": {
18
+ "coreai_torch": "0.4.3",
19
+ "coreai_core": "1.0.0b3",
20
+ "torch": "2.9.0",
21
+ "recipe": "export/export.py --mode ane --buckets 32,64,128,256,512,1024,2048",
22
+ "global_attention": "mqa4d",
23
+ "gqa": "grouped5d"
24
+ }
25
+ },
26
+ "modifications": [
27
+ "Text encoder only: the vision and audio encoders of the multimodal checkpoint are not included.",
28
+ "Architecture re-authored in plain PyTorch for Core AI export; weights are the original ones, rounded from bf16 to fp16.",
29
+ "The sqrt(512) token-embedding scale and the 512^-0.5 per-layer-embedding scale are folded into the tables.",
30
+ "The four global (full-attention, single-kv-head) layers compute attention as a plain 4-D matmul with k/v broadcast over the heads; the sliding-window layers use a 5-D grouped matmul. Mathematically identical to the reference (fp32 max abs diff 3e-7).",
31
+ "fp16 everywhere (Neural Engine): every RMSNorm and the final L2 normalization divide the row by its max-abs value before squaring, so activations (~2000) never overflow fp16; in the model code every division is expressed as rsqrt(x)*rsqrt(x) (the Neural Engine compiler rejects divide).",
32
+ "Bidirectional attention masks (padding + sliding window 512) are built inside the graph from attention_mask; the additive mask value is -10000 (finite).",
33
+ "Masked mean pooling, the 512->768 embedding projection and the L2 normalization run inside the graph; the output is the final sentence embedding.",
34
+ "Fixed sequence buckets 32, 64, 128, 256, 512, 1024, 2048 as functions main_<S>; input longer than 2048 tokens must be truncated or chunked by the caller (the original model accepts 8192)."
35
+ ]
36
+ }
model/config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_model": "google/embeddinggemma-2",
3
+ "format": "coreai",
4
+ "asset": "embeddinggemma-2-text.aimodel",
5
+ "functions": [
6
+ "main_32",
7
+ "main_64",
8
+ "main_128",
9
+ "main_256",
10
+ "main_512",
11
+ "main_1024",
12
+ "main_2048"
13
+ ],
14
+ "precision_mode": "ane",
15
+ "buckets": [
16
+ 32,
17
+ 64,
18
+ 128,
19
+ 256,
20
+ 512,
21
+ 1024,
22
+ 2048
23
+ ],
24
+ "max_tokens": 2048,
25
+ "inputs": {
26
+ "input_ids": "[1, S] int32 (bos=2 ... eos=1, right-padded with pad=0)",
27
+ "attention_mask": "[1, S] float16 (1 = token, 0 = padding)"
28
+ },
29
+ "output": {
30
+ "embedding": "[1, 768] float16, L2-normalized; Matryoshka: keep the first 512/256/128 and re-normalize"
31
+ },
32
+ "hidden_size": 512,
33
+ "embedding_dim": 768,
34
+ "matryoshka_dims": [
35
+ 768,
36
+ 512,
37
+ 256,
38
+ 128
39
+ ],
40
+ "bos_token_id": 2,
41
+ "eos_token_id": 1,
42
+ "pad_token_id": 0,
43
+ "prompts": {
44
+ "document": "title: none | text: ",
45
+ "search_query": "task: search result | query: ",
46
+ "question_answering": "task: question answering | query: ",
47
+ "fact_checking": "task: fact checking | query: ",
48
+ "code_retrieval": "task: code retrieval | query: ",
49
+ "classification": "task: classification | query: ",
50
+ "clustering": "task: clustering | query: ",
51
+ "sentence_similarity": "task: sentence similarity | query: "
52
+ },
53
+ "tokenizer": "tokenizer/tokenizer.json (unchanged Gemma tokenizer; the runner adds <bos> and <eos>)"
54
+ }
model/embeddinggemma-2-text.aimodel/main.hash ADDED
@@ -0,0 +1 @@
 
 
1
+ uӄ1��$q�T �3Wq
model/embeddinggemma-2-text.aimodel/main.mlirb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75d38431abcd2471b0540c863357710d305cb1687295f5b0cf859de6ac9d46eb
3
+ size 631529960
model/embeddinggemma-2-text.aimodel/metadata.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "assetVersion" : "2.0",
3
+ "producer" : "coreai-core 1.0.0b3",
4
+ "license" : "Apache-2.0",
5
+ "author" : "Gemma Team (Google); Core AI port",
6
+ "creationDate" : "20261012T021653Z",
7
+ "description" : "EmbeddingGemma 2 text encoder (google\/embeddinggemma-2) with mean pooling, 768-d projection and L2 normalization in-graph; precision mode ane; sequence buckets [32, 64, 128, 256, 512, 1024, 2048]."
8
+ }
model/tokenizer/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4d777ef5bdc1aa36227abdfb77c3e49e7b9c892d16e1b6bda41c393504828be4
3
+ size 32170510
model/tokenizer/tokenizer_config.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_token": "<|audio|>",
3
+ "backend": "tokenizers",
4
+ "boa_token": "<|audio>",
5
+ "boi_token": "<|image>",
6
+ "bos_token": "<bos>",
7
+ "eoa_token": "<audio|>",
8
+ "eoc_token": "<channel|>",
9
+ "eoi_token": "<image|>",
10
+ "eos_token": "<eos>",
11
+ "eot_token": "<turn|>",
12
+ "escape_token": "<|\"|>",
13
+ "etc_token": "<tool_call|>",
14
+ "etd_token": "<tool|>",
15
+ "etr_token": "<tool_response|>",
16
+ "extra_special_tokens": [
17
+ "<|video|>"
18
+ ],
19
+ "image_token": "<|image|>",
20
+ "is_local": true,
21
+ "local_files_only": false,
22
+ "mask_token": "<mask>",
23
+ "model_max_length": 1000000000000000019884624838656,
24
+ "model_specific_special_tokens": {
25
+ "audio_token": "<|audio|>",
26
+ "boa_token": "<|audio>",
27
+ "boi_token": "<|image>",
28
+ "eoa_token": "<audio|>",
29
+ "eoc_token": "<channel|>",
30
+ "eoi_token": "<image|>",
31
+ "eot_token": "<turn|>",
32
+ "escape_token": "<|\"|>",
33
+ "etc_token": "<tool_call|>",
34
+ "etd_token": "<tool|>",
35
+ "etr_token": "<tool_response|>",
36
+ "image_token": "<|image|>",
37
+ "soc_token": "<|channel>",
38
+ "sot_token": "<|turn>",
39
+ "stc_token": "<|tool_call>",
40
+ "std_token": "<|tool>",
41
+ "str_token": "<|tool_response>",
42
+ "think_token": "<|think|>",
43
+ "video_token": "<|video|>"
44
+ },
45
+ "pad_token": "<pad>",
46
+ "padding_side": "right",
47
+ "processor_class": "EmbeddingGemma2Processor",
48
+ "soc_token": "<|channel>",
49
+ "sot_token": "<|turn>",
50
+ "stc_token": "<|tool_call>",
51
+ "std_token": "<|tool>",
52
+ "str_token": "<|tool_response>",
53
+ "think_token": "<|think|>",
54
+ "tokenizer_class": "GemmaTokenizer",
55
+ "unk_token": "<unk>",
56
+ "video_token": "<|video|>"
57
+ }
python/embeddinggemma2_coreai.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
3
+ """EmbeddingGemma 2 (text) on Apple Core AI, macOS 27 — Python runner (library + CLI).
4
+
5
+ Library:
6
+
7
+ from embeddinggemma2_coreai import EmbeddingGemma2CoreAI, remove_own_mpsgraph_temp_files
8
+ m = EmbeddingGemma2CoreAI("../model", compute="ane") # ane | gpu | cpu | default
9
+ m.prewarm() # optional: one call per bucket
10
+ q = m.embed("When do the cloud credits expire?", kind="search_query") # 768-d, L2-normalized
11
+ d = m.embed("The credits expire on the last day of March.", kind="document", dim=256)
12
+ del m
13
+ remove_own_mpsgraph_temp_files() # see "Temp files" in the README
14
+
15
+ CLI:
16
+
17
+ python3 embeddinggemma2_coreai.py embed ../model [--compute ane] [--kind document] [--dim 768] "text" ...
18
+ python3 embeddinggemma2_coreai.py smoke ../model ../tests/smoke-reference.json [--compute ane] [--cleanup-temp]
19
+ python3 embeddinggemma2_coreai.py tokcheck ../model ../tests/tokenizer-cases.json
20
+ python3 embeddinggemma2_coreai.py latency ../model [--compute ane] [--trials 50]
21
+ python3 embeddinggemma2_coreai.py prewarm ../model [--compute ane]
22
+
23
+ Tokenization uses the Hugging Face `tokenizers` library on the bundle's tokenizer.json (the reference tokenizer).
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import asyncio
29
+ import ctypes
30
+ import json
31
+ import os
32
+ import sys
33
+ import time
34
+ from pathlib import Path
35
+
36
+ import numpy as np
37
+
38
+ _PROCESS_START = time.time()
39
+
40
+
41
+ class EmbeddingGemma2CoreAI:
42
+ """Loads the bundle folder (config.json, tokenizer/, the .aimodel) and embeds texts."""
43
+
44
+ def __init__(self, bundle: str | os.PathLike, compute: str = "ane", quiet: bool = True):
45
+ import coreai.runtime as rt
46
+ from tokenizers import Tokenizer
47
+
48
+ self._rt = rt
49
+ self.bundle = Path(bundle)
50
+ self.config = json.loads((self.bundle / "config.json").read_text(encoding="utf-8"))
51
+ t = time.perf_counter()
52
+ self.tokenizer = Tokenizer.from_file(str(self.bundle / "tokenizer" / "tokenizer.json"))
53
+ self.tokenizer_load_s = time.perf_counter() - t
54
+ self.compute = compute
55
+ if compute == "cpu":
56
+ opts = rt.SpecializationOptions.cpu_only()
57
+ elif compute == "gpu":
58
+ opts = rt.SpecializationOptions.from_preferred_compute_unit_kind(rt.ComputeUnitKind.gpu())
59
+ elif compute == "ane":
60
+ opts = rt.SpecializationOptions.from_preferred_compute_unit_kind(rt.ComputeUnitKind.neural_engine())
61
+ elif compute == "default":
62
+ opts = rt.SpecializationOptions.default()
63
+ else:
64
+ raise ValueError("compute must be ane | gpu | cpu | default")
65
+ t = time.perf_counter()
66
+ self.model = asyncio.run(rt.AIModel.load(self.bundle / self.config["asset"], opts))
67
+ self.functions: dict[int, object] = {}
68
+ for name in self.config["functions"]:
69
+ self.functions[int(name.rsplit("_", 1)[1])] = self.model.load_function(name)
70
+ self.buckets = sorted(self.functions)
71
+ self.load_s = time.perf_counter() - t
72
+ if not quiet:
73
+ print(f"loaded {self.bundle.name} compute={compute} buckets={self.buckets} in {self.load_s:.1f}s", flush=True)
74
+
75
+ # ---- tokenization -------------------------------------------------------------------------------------
76
+ def tokenize(self, text: str, kind: str | None = "document") -> list[int]:
77
+ """<bos> + prompt prefix + text + <eos>, truncated to the largest bucket (longer input is cut)."""
78
+ prefix = self.config["prompts"].get(kind, "") if kind else ""
79
+ ids = self.tokenizer.encode(prefix + text, add_special_tokens=False).ids
80
+ max_body = self.buckets[-1] - 2
81
+ if len(ids) > max_body:
82
+ ids = ids[:max_body]
83
+ return [self.config["bos_token_id"]] + ids + [self.config["eos_token_id"]]
84
+
85
+ def bucket(self, n_tokens: int) -> int:
86
+ for b in self.buckets:
87
+ if b >= n_tokens:
88
+ return b
89
+ raise ValueError(f"{n_tokens} tokens exceeds the largest bucket {self.buckets[-1]}")
90
+
91
+ # ---- inference ----------------------------------------------------------------------------------------
92
+ def embed_ids(self, ids: list[int], bucket: int | None = None) -> np.ndarray:
93
+ """Runs main_<bucket> on ids (with <bos>/<eos>), right-padded with <pad>. Returns the raw 768-d output."""
94
+ S = bucket or self.bucket(len(ids))
95
+ if len(ids) > S:
96
+ raise ValueError(f"{len(ids)} ids do not fit bucket {S}")
97
+ x = np.full((1, S), self.config["pad_token_id"], dtype=np.int32)
98
+ x[0, : len(ids)] = ids
99
+ m = np.zeros((1, S), dtype=np.float16)
100
+ m[0, : len(ids)] = 1
101
+ out = asyncio.run(self.functions[S](inputs={"input_ids": self._rt.NDArray(x), "attention_mask": self._rt.NDArray(m)}))
102
+ return np.array(out["embedding"].numpy(), dtype=np.float32).reshape(-1)
103
+
104
+ def embed(self, text: str, kind: str | None = "document", dim: int = 768) -> np.ndarray:
105
+ """L2-normalized embedding. dim 768, or a Matryoshka size (512/256/128): leading dims, re-normalized."""
106
+ if dim not in self.config["matryoshka_dims"]:
107
+ raise ValueError(f"dim must be one of {self.config['matryoshka_dims']}")
108
+ v = self.embed_ids(self.tokenize(text, kind))[:dim]
109
+ return v / max(float(np.linalg.norm(v)), 1e-12)
110
+
111
+ def prewarm(self) -> float:
112
+ """One call per bucket so the first real call pays no specialization/first-call cost. Returns seconds."""
113
+ t = time.perf_counter()
114
+ for S in self.buckets:
115
+ self.embed_ids([self.config["bos_token_id"], self.config["eos_token_id"]], S)
116
+ return time.perf_counter() - t
117
+
118
+ def close(self) -> None:
119
+ self.functions.clear()
120
+ self.model = None
121
+
122
+
123
+ # ---- temp-file hygiene ----------------------------------------------------------------------------------
124
+ def mpsgraph_temp_root() -> Path:
125
+ import tempfile
126
+
127
+ return Path(os.environ.get("TMPDIR") or tempfile.gettempdir()) / "com.apple.MetalPerformanceShadersGraph"
128
+
129
+
130
+ def remove_own_mpsgraph_temp_files(pid: int | None = None, since: float | None = None) -> tuple[int, int]:
131
+ """Each Neural Engine load of this model makes MPSGraph write ~358 MB of compiled-graph copies into
132
+ $TMPDIR/com.apple.MetalPerformanceShadersGraph/mpsgraph-<pid>-<date>-<n>/, and the Python runtime (coreai.runtime,
133
+ macOS 27.0) does NOT delete them when the model is released or the interpreter exits. This removes the folders of the CURRENT process only (its pid, created after
134
+ it started). Call it after the model object has been released, or at exit. Returns (folders, bytes)."""
135
+ import shutil
136
+
137
+ pid = pid or os.getpid()
138
+ since = _PROCESS_START - 2 if since is None else since
139
+ root = mpsgraph_temp_root()
140
+ folders = nbytes = 0
141
+ if not root.is_dir():
142
+ return 0, 0
143
+ for d in root.iterdir():
144
+ if not d.name.startswith(f"mpsgraph-{pid}-") or not d.is_dir():
145
+ continue
146
+ try:
147
+ if d.stat().st_birthtime < since:
148
+ continue # left by an older process that happened to have the same pid
149
+ nbytes += sum(f.stat().st_size for f in d.rglob("*") if f.is_file())
150
+ shutil.rmtree(d)
151
+ folders += 1
152
+ except OSError:
153
+ pass
154
+ return folders, nbytes
155
+
156
+
157
+ # ---- process accounting helpers (macOS) ------------------------------------------------------------------
158
+ def _rusage_v6():
159
+ class RU6(ctypes.Structure):
160
+ _fields_ = [("uuid", ctypes.c_uint8 * 16)] + [(f"f{i}", ctypes.c_uint64) for i in range(47)] + [("reserved", ctypes.c_uint64 * 9)]
161
+
162
+ libc = ctypes.CDLL("/usr/lib/libSystem.B.dylib")
163
+ r = RU6()
164
+ return r if libc.proc_pid_rusage(os.getpid(), 6, ctypes.byref(r)) == 0 else None
165
+
166
+
167
+ def footprints_mb() -> tuple[float, float]:
168
+ """(lifetime max physical footprint, Neural Engine footprint) in MB; the latter is > 0 only if the ANE ran."""
169
+ r = _rusage_v6()
170
+ if r is None:
171
+ return -1.0, -1.0
172
+ return r.f28 / 2**20, r.f44 / 2**20 # ri_lifetime_max_phys_footprint, ri_neural_footprint
173
+
174
+
175
+ def cosine(a, b) -> float:
176
+ a = np.asarray(a, np.float64)
177
+ b = np.asarray(b, np.float64)
178
+ return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
179
+
180
+
181
+ # ---- CLI -------------------------------------------------------------------------------------------------
182
+ def cmd_embed(a) -> int:
183
+ m = EmbeddingGemma2CoreAI(a.bundle, a.compute)
184
+ for t in a.args:
185
+ v = m.embed(t, None if a.kind == "none" else a.kind, a.dim)
186
+ print(json.dumps({"text": t, "kind": a.kind, "dim": int(v.shape[0]), "embedding": [round(float(x), 6) for x in v]}, ensure_ascii=False))
187
+ m.close()
188
+ return 0
189
+
190
+
191
+ def cmd_smoke(a) -> int:
192
+ ref = json.loads(Path(a.args[0]).read_text(encoding="utf-8"))
193
+ m = EmbeddingGemma2CoreAI(a.bundle, a.compute)
194
+ print(f"loaded in {m.load_s:.1f} s (tokenizer {m.tokenizer_load_s:.2f} s), compute={a.compute}, buckets={m.buckets}")
195
+ ok = True
196
+ for c in ref["cases"]:
197
+ v = m.embed(c["text"], c["kind"])
198
+ cos = cosine(v, c["embedding"])
199
+ passed = cos >= ref["threshold_cosine"] and bool(np.all(np.isfinite(v)))
200
+ ok = ok and passed
201
+ print(f"{'PASS' if passed else 'FAIL'} cosine {cos:.6f} (threshold {ref['threshold_cosine']}) {c['kind']}: {c['text'][:60]}")
202
+ fp, nf = footprints_mb()
203
+ print(f"peak footprint {fp:.0f} MB, neural footprint {nf:.0f} MB")
204
+ print("SMOKE PASS" if ok else "SMOKE FAIL")
205
+ m.close()
206
+ return 0 if ok else 1
207
+
208
+
209
+ def cmd_tokcheck(a) -> int:
210
+ from tokenizers import Tokenizer
211
+
212
+ cases = json.loads(Path(a.args[0]).read_text(encoding="utf-8"))
213
+ tok = Tokenizer.from_file(str(Path(a.bundle) / "tokenizer" / "tokenizer.json"))
214
+ prefixes = {"q": "task: search result | query: ", "d": "title: none | text: ", "raw": ""}
215
+ same = 0
216
+ t = time.perf_counter()
217
+ for c in cases:
218
+ ids = [2] + tok.encode(prefixes[c["kind"]] + c["text"], add_special_tokens=False).ids + [1]
219
+ same += ids == c["ids"]
220
+ print(f"TOKCHECK {same}/{len(cases)} identical (tokenizers {__import__('tokenizers').__version__}) in {time.perf_counter() - t:.2f} s")
221
+ return 0 if same == len(cases) else 1
222
+
223
+
224
+ def cmd_latency(a) -> int:
225
+ m = EmbeddingGemma2CoreAI(a.bundle, a.compute)
226
+ print(f"loaded in {m.load_s:.1f} s, compute={a.compute}; prewarm {m.prewarm():.2f} s")
227
+ print("bucket p50 ms min ms p90 ms CPU ms/call tok/s")
228
+ for S in m.buckets:
229
+ ids = [m.config["bos_token_id"]] + [1000 + (i * 7919) % 200000 for i in range(1, S - 1)] + [m.config["eos_token_id"]]
230
+ for _ in range(3):
231
+ m.embed_ids(ids, S)
232
+ ts = []
233
+ c0 = time.process_time()
234
+ for _ in range(a.trials):
235
+ t = time.perf_counter()
236
+ m.embed_ids(ids, S)
237
+ ts.append((time.perf_counter() - t) * 1000)
238
+ cpu = (time.process_time() - c0) / a.trials * 1000
239
+ p50 = float(np.median(ts))
240
+ print(f"{S:5d} {p50:7.2f} {min(ts):7.2f} {float(np.percentile(ts, 90)):7.2f} {cpu:11.2f} {S / p50 * 1000:6.0f}")
241
+ fp, nf = footprints_mb()
242
+ print(f"peak footprint {fp:.0f} MB, neural footprint {nf:.0f} MB ({'Neural Engine in use' if nf > 0 else 'Neural Engine NOT used'})")
243
+ m.close()
244
+ return 0
245
+
246
+
247
+ def cmd_prewarm(a) -> int:
248
+ t0 = time.perf_counter()
249
+ m = EmbeddingGemma2CoreAI(a.bundle, a.compute)
250
+ w = m.prewarm()
251
+ print(f"load {m.load_s:.1f} s + prewarm {w:.2f} s = ready in {time.perf_counter() - t0:.1f} s")
252
+ m.close()
253
+ return 0
254
+
255
+
256
+ def main() -> None:
257
+ ap = argparse.ArgumentParser(description="EmbeddingGemma 2 (text) on Apple Core AI")
258
+ ap.add_argument("cmd", choices=["embed", "smoke", "tokcheck", "latency", "prewarm"])
259
+ ap.add_argument("bundle", help="the model folder (config.json, tokenizer/, *.aimodel)")
260
+ ap.add_argument("args", nargs="*", help="texts (embed) or a json file (smoke, tokcheck)")
261
+ ap.add_argument("--compute", default="ane", choices=["ane", "gpu", "cpu", "default"])
262
+ ap.add_argument("--kind", default="document", help="prompt kind from config.json (document, search_query, ...) or none")
263
+ ap.add_argument("--dim", type=int, default=768)
264
+ ap.add_argument("--trials", type=int, default=50)
265
+ ap.add_argument("--cleanup-temp", action="store_true", help="remove this process's MPSGraph temp folders at exit")
266
+ a = ap.parse_args()
267
+ code = {"embed": cmd_embed, "smoke": cmd_smoke, "tokcheck": cmd_tokcheck, "latency": cmd_latency, "prewarm": cmd_prewarm}[a.cmd](a)
268
+ if a.cleanup_temp:
269
+ import gc
270
+
271
+ gc.collect()
272
+ n, b = remove_own_mpsgraph_temp_files()
273
+ print(f"cleanup-temp: removed {n} MPSGraph temp folder(s), {b / 2**20:.1f} MB")
274
+ sys.exit(code)
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()
python/requirements.lock ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exact versions of the environments this release was validated in (macOS 27.0, Apple silicon).
2
+ # Runtime (python/), Python 3.12 or 3.14:
3
+ coreai-core==1.0.0b3
4
+ numpy==2.4.6
5
+ tokenizers==0.23.3
6
+ huggingface-hub==2.2.0
7
+ # Exporter (export/), on top of the runtime, Python 3.12:
8
+ coreai-torch==0.4.3
9
+ coreai-opt==0.3.0
10
+ torch==2.9.0
11
+ torchao==0.17.0
12
+ safetensors==0.8.0
13
+ ml-dtypes==0.6.0
14
+ networkx==3.7
15
+ scipy==1.18.1
16
+ packaging==26.3
17
+ # Reference vectors (tests/make_smoke_reference.py), a separate environment:
18
+ # sentence-transformers==6.1.0 transformers==5.19.0 torch==2.14.1 torchvision==0.29.1 pillow tokenizers==0.23.3 numpy==2.5.3
19
+ # (pillow and torchvision are needed because the checkpoint's processor is multimodal, even for text-only use)
python/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Runtime requirements for python/ (macOS 27, Apple silicon; validated with Python 3.12 and 3.14).
2
+ # coreai-core provides the `coreai.runtime` module (Apple's Core AI Python runtime); install it from the same
3
+ # distribution that provides coreai-torch. The exact versions this release was validated with are in
4
+ # requirements.lock.
5
+ coreai-core==1.0.0b3
6
+ numpy>=2.0,<2.5
7
+ tokenizers>=0.22,<0.24
python/smoke_test.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
3
+ """Smoke test: load the bundle, embed three fictional sentences, compare with the shipped fp32 reference vectors.
4
+
5
+ python3 smoke_test.py [--bundle ../model] [--reference ../tests/smoke-reference.json] [--compute ane] [--cleanup-temp]
6
+
7
+ Exit code 0 = every cosine >= threshold (0.999) and all values finite. The first run of a new executable (here: your
8
+ python binary) specializes the model for the Neural Engine: expect up to ~145 s (139-144 s measured). Later runs load
9
+ in 0.4-1.5 s.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ HERE = Path(__file__).resolve().parent
18
+ sys.path.insert(0, str(HERE))
19
+ from embeddinggemma2_coreai import EmbeddingGemma2CoreAI, cosine, footprints_mb, remove_own_mpsgraph_temp_files # noqa: E402
20
+
21
+
22
+ def main() -> int:
23
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
24
+ ap.add_argument("--bundle", default=str(HERE.parent / "model"))
25
+ ap.add_argument("--reference", default=str(HERE.parent / "tests" / "smoke-reference.json"))
26
+ ap.add_argument("--compute", default="ane", choices=["ane", "gpu", "cpu", "default"])
27
+ ap.add_argument("--cleanup-temp", action="store_true", help="remove this process's MPSGraph temp folders at exit")
28
+ a = ap.parse_args()
29
+ import json
30
+
31
+ ref = json.loads(Path(a.reference).read_text(encoding="utf-8"))
32
+ m = EmbeddingGemma2CoreAI(a.bundle, a.compute)
33
+ print(f"loaded in {m.load_s:.1f} s (tokenizer {m.tokenizer_load_s:.2f} s), compute={a.compute}, buckets={m.buckets}")
34
+ ok = True
35
+ for c in ref["cases"]:
36
+ v = m.embed(c["text"], c["kind"])
37
+ cos = cosine(v, c["embedding"])
38
+ passed = cos >= ref["threshold_cosine"] and all(map(lambda x: x == x and abs(x) != float("inf"), v.tolist()))
39
+ ok = ok and passed
40
+ print(f"{'PASS' if passed else 'FAIL'} cosine {cos:.6f} (threshold {ref['threshold_cosine']}) {c['kind']}: {c['text'][:60]}")
41
+ # Matryoshka prefixes must stay close to the truncated+renormalized reference too
42
+ for dim in (512, 256, 128):
43
+ c = ref["cases"][1]
44
+ v = m.embed(c["text"], c["kind"], dim=dim)
45
+ r = c["embedding"][:dim]
46
+ print(f"{'PASS' if cosine(v, r) >= ref['threshold_cosine'] else 'FAIL'} cosine {cosine(v, r):.6f} at dim {dim}")
47
+ fp, nf = footprints_mb()
48
+ print(f"peak footprint {fp:.0f} MB, neural footprint {nf:.0f} MB ({'Neural Engine in use' if nf > 0 else 'Neural Engine not used'})")
49
+ print("SMOKE PASS" if ok else "SMOKE FAIL")
50
+ m.close()
51
+ del m
52
+ if a.cleanup_temp:
53
+ import gc
54
+
55
+ gc.collect()
56
+ n, b = remove_own_mpsgraph_temp_files()
57
+ print(f"cleanup-temp: removed {n} MPSGraph temp folder(s), {b / 2**20:.1f} MB")
58
+ return 0 if ok else 1
59
+
60
+
61
+ if __name__ == "__main__":
62
+ sys.exit(main())
swift/Package.swift ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // swift-tools-version: 6.0
2
+ // Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
3
+ import PackageDescription
4
+
5
+ let package = Package(
6
+ name: "EmbeddingGemma2CoreAI",
7
+ platforms: [.macOS("27.0")],
8
+ products: [
9
+ .library(name: "EmbeddingGemma2CoreAI", targets: ["EmbeddingGemma2CoreAI"]),
10
+ .executable(name: "eg2-cli", targets: ["eg2-cli"]),
11
+ ],
12
+ targets: [
13
+ // Library: exact Gemma tokenizer + Core AI embedder. No third-party dependencies.
14
+ .target(
15
+ name: "EmbeddingGemma2CoreAI",
16
+ path: "Sources/EmbeddingGemma2CoreAI",
17
+ linkerSettings: [.linkedFramework("CoreAI")]
18
+ ),
19
+ // Command-line tool: embed | smoke | tokcheck | latency | prewarm | cleanup-temp
20
+ .executableTarget(
21
+ name: "eg2-cli",
22
+ dependencies: ["EmbeddingGemma2CoreAI"],
23
+ path: "Sources/eg2-cli"
24
+ ),
25
+ ]
26
+ )
swift/Sources/EmbeddingGemma2CoreAI/Embedder.swift ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE).
2
+ //
3
+ // EmbeddingGemma 2 (text) on Apple Core AI, macOS 27. Loads the bundle folder (config.json, tokenizer/, the
4
+ // .aimodel), tokenizes exactly like Hugging Face `tokenizers`, pads to the smallest sequence bucket, runs the
5
+ // matching `main_<S>` function and returns the L2-normalized 768-d embedding (or a Matryoshka prefix).
6
+
7
+ import CoreAI
8
+ import Foundation
9
+
10
+ public struct BundleConfig: Decodable, Sendable {
11
+ public let asset: String
12
+ public let functions: [String]
13
+ public let buckets: [Int]
14
+ public let max_tokens: Int
15
+ public let embedding_dim: Int
16
+ public let matryoshka_dims: [Int]
17
+ public let prompts: [String: String]
18
+ public let bos_token_id: Int32
19
+ public let eos_token_id: Int32
20
+ public let pad_token_id: Int32
21
+ }
22
+
23
+ public enum ComputePreference: String, Sendable {
24
+ case neuralEngine = "ane"
25
+ case gpu
26
+ case cpu
27
+ case `default`
28
+ }
29
+
30
+ public struct EmbedderError: Error, CustomStringConvertible {
31
+ public let description: String
32
+ }
33
+
34
+ public final class EmbeddingGemma2 {
35
+ public let bundleURL: URL
36
+ public let config: BundleConfig
37
+ public let tokenizer: GemmaTokenizer
38
+ public let compute: ComputePreference
39
+ /// Wall-clock seconds spent in `AIModel(contentsOf:)` + `loadFunction` (specialization). 0.4–1.5 s warm;
40
+ /// the first load by an executable that has not loaded this bundle before takes up to ~145 s (see README).
41
+ public let loadSeconds: Double
42
+ public let tokenizerLoadSeconds: Double
43
+
44
+ private let model: AIModel
45
+ private let functions: [Int: InferenceFunction] // bucket -> main_<S>
46
+ public var buckets: [Int] { functions.keys.sorted() }
47
+
48
+ public init(bundle: URL, compute: ComputePreference = .neuralEngine) async throws {
49
+ bundleURL = bundle
50
+ self.compute = compute
51
+ config = try JSONDecoder().decode(BundleConfig.self, from: Data(contentsOf: bundle.appendingPathComponent("config.json")))
52
+ var t0 = Date()
53
+ tokenizer = try GemmaTokenizer(contentsOf: bundle.appendingPathComponent("tokenizer/tokenizer.json"))
54
+ tokenizerLoadSeconds = Date().timeIntervalSince(t0)
55
+ guard tokenizer.bosID == config.bos_token_id, tokenizer.eosID == config.eos_token_id, tokenizer.padID == config.pad_token_id else {
56
+ throw EmbedderError(description: "tokenizer special ids do not match config.json")
57
+ }
58
+ t0 = Date()
59
+ let options: SpecializationOptions
60
+ switch compute {
61
+ case .gpu: options = SpecializationOptions(preferredComputeUnitKind: .gpu)
62
+ case .cpu: options = SpecializationOptions.cpuOnly
63
+ case .neuralEngine: options = SpecializationOptions(preferredComputeUnitKind: .neuralEngine)
64
+ case .default: options = SpecializationOptions.default
65
+ }
66
+ model = try await AIModel(contentsOf: bundle.appendingPathComponent(config.asset), options: options)
67
+ var fns: [Int: InferenceFunction] = [:]
68
+ for name in config.functions {
69
+ guard let s = Int(name.split(separator: "_").last ?? ""), let fn = try model.loadFunction(named: name) else {
70
+ throw EmbedderError(description: "function \(name) missing from \(config.asset)")
71
+ }
72
+ fns[s] = fn
73
+ }
74
+ functions = fns
75
+ loadSeconds = Date().timeIntervalSince(t0)
76
+ }
77
+
78
+ // MARK: - Tokenization
79
+
80
+ /// `<bos>` + prompt prefix + text + `<eos>`, truncated to the largest bucket (2048 tokens: longer input is cut).
81
+ /// `kind` is a key of `config.prompts` ("document", "search_query", ...); nil = no prefix.
82
+ public func tokenize(_ text: String, kind: String? = "document") -> [Int32] {
83
+ let prefix = kind.flatMap { config.prompts[$0] } ?? ""
84
+ var ids = tokenizer.encode(prefix + text)
85
+ let maxBody = (buckets.last ?? 32) - 2
86
+ if ids.count > maxBody { ids = Array(ids.prefix(maxBody)) }
87
+ return [config.bos_token_id] + ids + [config.eos_token_id]
88
+ }
89
+
90
+ public func bucket(forTokenCount n: Int) throws -> Int {
91
+ guard let b = buckets.first(where: { $0 >= n }) else {
92
+ throw EmbedderError(description: "\(n) tokens exceeds the largest bucket \(buckets.last ?? 0)")
93
+ }
94
+ return b
95
+ }
96
+
97
+ // MARK: - Inference
98
+
99
+ /// Runs `main_<bucket>` on already-tokenized ids (with `<bos>`/`<eos>`), right-padded with `<pad>`.
100
+ /// Returns the raw 768-d output (already L2-normalized by the graph), as Float.
101
+ public func embed(ids: [Int32], bucket: Int? = nil) async throws -> [Float] {
102
+ let S = try bucket ?? self.bucket(forTokenCount: ids.count)
103
+ guard let fn = functions[S] else { throw EmbedderError(description: "no function for bucket \(S)") }
104
+ guard ids.count <= S else { throw EmbedderError(description: "\(ids.count) ids do not fit bucket \(S)") }
105
+ var inputIDs = NDArray(shape: [1, S], scalarType: .int32)
106
+ var mask = NDArray(shape: [1, S], scalarType: .float16)
107
+ do {
108
+ var v = inputIDs.mutableView(as: Int32.self)
109
+ var m = mask.mutableView(as: Float16.self)
110
+ let pad = config.pad_token_id
111
+ v.withUnsafeMutablePointer { p, _, _ in
112
+ for i in 0..<S { p[i] = i < ids.count ? ids[i] : pad }
113
+ }
114
+ m.withUnsafeMutablePointer { p, _, _ in
115
+ for i in 0..<S { p[i] = i < ids.count ? 1 : 0 }
116
+ }
117
+ }
118
+ var out = try await fn.run(inputs: ["input_ids": inputIDs, "attention_mask": mask])
119
+ guard let emb = out.remove("embedding")?.ndArray else { throw EmbedderError(description: "output 'embedding' missing") }
120
+ let dim = config.embedding_dim
121
+ var result = [Float](repeating: 0, count: dim)
122
+ let view = emb.view(as: Float16.self)
123
+ view.withUnsafePointer { p, _, _ in
124
+ for i in 0..<dim { result[i] = Float(p[i]) }
125
+ }
126
+ return result
127
+ }
128
+
129
+ /// Embeds one text. `dim` 768 (full) or a Matryoshka size 512/256/128: the leading dims, re-normalized.
130
+ public func embed(_ text: String, kind: String? = "document", dim: Int = 768) async throws -> [Float] {
131
+ guard config.matryoshka_dims.contains(dim) else { throw EmbedderError(description: "dim must be one of \(config.matryoshka_dims)") }
132
+ var v = Array(try await embed(ids: tokenize(text, kind: kind)).prefix(dim))
133
+ let norm = max(sqrt(v.reduce(0) { $0 + $1 * $1 }), 1e-12)
134
+ for i in v.indices { v[i] /= norm }
135
+ return v
136
+ }
137
+
138
+ /// Runs every bucket once so the first real call pays no specialization/first-call cost. Call it right after
139
+ /// init, off the main thread. Returns the seconds it took.
140
+ @discardableResult
141
+ public func prewarm() async throws -> Double {
142
+ let t0 = Date()
143
+ for S in buckets {
144
+ _ = try await embed(ids: [config.bos_token_id, config.eos_token_id], bucket: S)
145
+ }
146
+ return Date().timeIntervalSince(t0)
147
+ }
148
+
149
+ // MARK: - Temp-file hygiene
150
+
151
+ /// Each Neural Engine load of this model makes MPSGraph write ~358 MB of compiled graph copies into
152
+ /// `$TMPDIR/com.apple.MetalPerformanceShadersGraph/mpsgraph-<pid>-<date>-<n>/`. With the Swift `CoreAI`
153
+ /// framework (macOS 27.0) the runtime deletes that folder itself when the model is released, so this usually
154
+ /// finds nothing; it is a safety net for a folder that survives (e.g. the process exits or the model is still
155
+ /// alive). It removes only the folders that belong to the CURRENT process (its pid, created after it started).
156
+ /// Call it after the `EmbeddingGemma2` instance has been released, or at exit. Returns the number of folders
157
+ /// removed and their total bytes.
158
+ @discardableResult
159
+ public static func removeOwnMPSGraphTempFiles() -> (folders: Int, bytes: Int64) {
160
+ let fm = FileManager.default
161
+ let root = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.apple.MetalPerformanceShadersGraph")
162
+ let prefix = "mpsgraph-\(getpid())-"
163
+ let started = processStartDate()
164
+ var folders = 0
165
+ var bytes: Int64 = 0
166
+ guard let entries = try? fm.contentsOfDirectory(at: root, includingPropertiesForKeys: [.creationDateKey]) else { return (0, 0) }
167
+ for url in entries where url.lastPathComponent.hasPrefix(prefix) {
168
+ if let started, let created = (try? url.resourceValues(forKeys: [.creationDateKey]))?.creationDate, created < started - 2 {
169
+ continue // a folder left by an older process that happened to have the same pid
170
+ }
171
+ if let e = fm.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey]) {
172
+ for case let f as URL in e { bytes += Int64((try? f.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0) }
173
+ }
174
+ if (try? fm.removeItem(at: url)) != nil { folders += 1 }
175
+ }
176
+ return (folders, bytes)
177
+ }
178
+
179
+ private static func processStartDate() -> Date? {
180
+ var info = kinfo_proc()
181
+ var size = MemoryLayout<kinfo_proc>.stride
182
+ var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
183
+ guard sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) == 0 else { return nil }
184
+ let tv = info.kp_proc.p_starttime
185
+ return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1e6)
186
+ }
187
+ }
188
+
189
+ // MARK: - Small helpers for callers and tests
190
+
191
+ public func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Double {
192
+ var dot = 0.0, na = 0.0, nb = 0.0
193
+ for i in 0..<min(a.count, b.count) {
194
+ dot += Double(a[i]) * Double(b[i]); na += Double(a[i]) * Double(a[i]); nb += Double(b[i]) * Double(b[i])
195
+ }
196
+ return na > 0 && nb > 0 ? dot / (sqrt(na) * sqrt(nb)) : 0
197
+ }
swift/Sources/EmbeddingGemma2CoreAI/ExactJSON.swift ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE).
2
+ //
3
+ // A small, exact JSON reader, written for this package and used to read tokenizer.json. Strings are returned as
4
+ // the exact UTF-8 bytes they encode (every \uXXXX escape, including surrogate pairs, is decoded to UTF-8; nothing
5
+ // is normalized or stripped). It exists because Foundation's JSONSerialization (macOS 27.0) drops a leading U+FEFF
6
+ // (byte-order mark) from every string VALUE, which turns the vocabulary entries "", "", ...
7
+ // into "", "", ... and breaks the tokenizer. (JSONDecoder does not have the bug, but a 32 MB vocabulary
8
+ // decoded through Codable is slow, and this reader keeps the bytes exact by construction.)
9
+ //
10
+ // Supported: the full JSON grammar (RFC 8259). Numbers are returned as Double. Object keys keep their order and
11
+ // duplicates (the vocabulary is read as an ordered list of pairs).
12
+
13
+ import Foundation
14
+
15
+ public indirect enum ExactJSON {
16
+ case object([(key: [UInt8], value: ExactJSON)])
17
+ case array([ExactJSON])
18
+ case string([UInt8])
19
+ case number(Double)
20
+ case bool(Bool)
21
+ case null
22
+
23
+ public struct ParseError: Error, CustomStringConvertible { public let description: String }
24
+
25
+ public static func parse(_ data: Data) throws -> ExactJSON {
26
+ try data.withUnsafeBytes { raw -> ExactJSON in
27
+ var p = Parser(bytes: raw.bindMemory(to: UInt8.self))
28
+ p.skipWS()
29
+ let v = try p.value()
30
+ p.skipWS()
31
+ guard p.i == p.bytes.count else { throw ParseError(description: "trailing characters at byte \(p.i)") }
32
+ return v
33
+ }
34
+ }
35
+
36
+ public static func parse(contentsOf url: URL) throws -> ExactJSON { try parse(Data(contentsOf: url)) }
37
+
38
+ // MARK: accessors
39
+ public subscript(key: String) -> ExactJSON? {
40
+ guard case .object(let pairs) = self else { return nil }
41
+ let k = Array(key.utf8)
42
+ for p in pairs where p.key == k { return p.value }
43
+ return nil
44
+ }
45
+ public var stringBytes: [UInt8]? { if case .string(let b) = self { return b }; return nil }
46
+ public var string: String? { stringBytes.map { String(decoding: $0, as: UTF8.self) } }
47
+ public var double: Double? { if case .number(let d) = self { return d }; return nil }
48
+ public var int: Int? { double.flatMap { $0 == $0.rounded() && abs($0) < 9e15 ? Int($0) : nil } }
49
+ public var bool: Bool? { if case .bool(let b) = self { return b }; return nil }
50
+ public var isNull: Bool { if case .null = self { return true }; return false }
51
+ public var array: [ExactJSON]? { if case .array(let a) = self { return a }; return nil }
52
+ public var pairs: [(key: [UInt8], value: ExactJSON)]? { if case .object(let p) = self { return p }; return nil }
53
+
54
+ // MARK: parser
55
+ private struct Parser {
56
+ let bytes: UnsafeBufferPointer<UInt8>
57
+ var i = 0
58
+
59
+ init(bytes: UnsafeBufferPointer<UInt8>) { self.bytes = bytes }
60
+
61
+ mutating func skipWS() {
62
+ while i < bytes.count, bytes[i] == 0x20 || bytes[i] == 0x0A || bytes[i] == 0x0D || bytes[i] == 0x09 { i += 1 }
63
+ }
64
+
65
+ func fail(_ what: String) -> ParseError { ParseError(description: "\(what) at byte \(i)") }
66
+
67
+ mutating func value() throws -> ExactJSON {
68
+ guard i < bytes.count else { throw fail("unexpected end") }
69
+ switch bytes[i] {
70
+ case 0x7B: return try object()
71
+ case 0x5B: return try array()
72
+ case 0x22: return .string(try string())
73
+ case 0x74: try literal("true"); return .bool(true)
74
+ case 0x66: try literal("false"); return .bool(false)
75
+ case 0x6E: try literal("null"); return .null
76
+ default: return .number(try number())
77
+ }
78
+ }
79
+
80
+ mutating func literal(_ s: StaticString) throws {
81
+ let n = s.utf8CodeUnitCount
82
+ guard i + n <= bytes.count else { throw fail("bad literal") }
83
+ for k in 0..<n where bytes[i + k] != s.utf8Start[k] { throw fail("bad literal") }
84
+ i += n
85
+ }
86
+
87
+ mutating func number() throws -> Double {
88
+ let start = i
89
+ while i < bytes.count {
90
+ let c = bytes[i]
91
+ if (c >= 0x30 && c <= 0x39) || c == 0x2D || c == 0x2B || c == 0x2E || c == 0x65 || c == 0x45 { i += 1 } else { break }
92
+ }
93
+ guard i > start, let d = Double(String(decoding: bytes[start..<i], as: UTF8.self)) else { throw fail("bad number") }
94
+ return d
95
+ }
96
+
97
+ mutating func object() throws -> ExactJSON {
98
+ i += 1
99
+ var pairs: [(key: [UInt8], value: ExactJSON)] = []
100
+ skipWS()
101
+ if i < bytes.count, bytes[i] == 0x7D { i += 1; return .object(pairs) }
102
+ while true {
103
+ skipWS()
104
+ guard i < bytes.count, bytes[i] == 0x22 else { throw fail("expected key") }
105
+ let k = try string()
106
+ skipWS()
107
+ guard i < bytes.count, bytes[i] == 0x3A else { throw fail("expected ':'") }
108
+ i += 1
109
+ skipWS()
110
+ pairs.append((k, try value()))
111
+ skipWS()
112
+ guard i < bytes.count else { throw fail("unterminated object") }
113
+ if bytes[i] == 0x2C { i += 1; continue }
114
+ if bytes[i] == 0x7D { i += 1; return .object(pairs) }
115
+ throw fail("expected ',' or '}'")
116
+ }
117
+ }
118
+
119
+ mutating func array() throws -> ExactJSON {
120
+ i += 1
121
+ var items: [ExactJSON] = []
122
+ skipWS()
123
+ if i < bytes.count, bytes[i] == 0x5D { i += 1; return .array(items) }
124
+ while true {
125
+ skipWS()
126
+ items.append(try value())
127
+ skipWS()
128
+ guard i < bytes.count else { throw fail("unterminated array") }
129
+ if bytes[i] == 0x2C { i += 1; continue }
130
+ if bytes[i] == 0x5D { i += 1; return .array(items) }
131
+ throw fail("expected ',' or ']'")
132
+ }
133
+ }
134
+
135
+ mutating func hex4() throws -> UInt32 {
136
+ guard i + 4 <= bytes.count else { throw fail("bad \\u escape") }
137
+ var v: UInt32 = 0
138
+ for _ in 0..<4 {
139
+ let c = bytes[i]
140
+ let d: UInt32
141
+ switch c {
142
+ case 0x30...0x39: d = UInt32(c - 0x30)
143
+ case 0x41...0x46: d = UInt32(c - 0x41 + 10)
144
+ case 0x61...0x66: d = UInt32(c - 0x61 + 10)
145
+ default: throw fail("bad hex digit")
146
+ }
147
+ v = v << 4 | d
148
+ i += 1
149
+ }
150
+ return v
151
+ }
152
+
153
+ /// Decodes a JSON string starting at the opening quote into exact UTF-8 bytes.
154
+ mutating func string() throws -> [UInt8] {
155
+ i += 1
156
+ var out: [UInt8] = []
157
+ while true {
158
+ guard i < bytes.count else { throw fail("unterminated string") }
159
+ let c = bytes[i]
160
+ if c == 0x22 { i += 1; return out }
161
+ if c != 0x5C { out.append(c); i += 1; continue }
162
+ i += 1
163
+ guard i < bytes.count else { throw fail("unterminated escape") }
164
+ let e = bytes[i]
165
+ i += 1
166
+ switch e {
167
+ case 0x22: out.append(0x22)
168
+ case 0x5C: out.append(0x5C)
169
+ case 0x2F: out.append(0x2F)
170
+ case 0x62: out.append(0x08)
171
+ case 0x66: out.append(0x0C)
172
+ case 0x6E: out.append(0x0A)
173
+ case 0x72: out.append(0x0D)
174
+ case 0x74: out.append(0x09)
175
+ case 0x75:
176
+ var cp = try hex4()
177
+ if cp >= 0xD800, cp <= 0xDBFF, i + 6 <= bytes.count, bytes[i] == 0x5C, bytes[i + 1] == 0x75 {
178
+ let save = i
179
+ i += 2
180
+ let lo = try hex4()
181
+ if lo >= 0xDC00, lo <= 0xDFFF {
182
+ cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00)
183
+ } else {
184
+ i = save // not a pair: keep the lone surrogate (encoded below as 3 bytes, like Rust would reject; never occurs in our files)
185
+ }
186
+ }
187
+ appendUTF8(cp, to: &out)
188
+ default: throw fail("bad escape")
189
+ }
190
+ }
191
+ }
192
+
193
+ func appendUTF8(_ cp: UInt32, to out: inout [UInt8]) {
194
+ switch cp {
195
+ case 0..<0x80: out.append(UInt8(cp))
196
+ case 0x80..<0x800: out.append(UInt8(0xC0 | cp >> 6)); out.append(UInt8(0x80 | cp & 0x3F))
197
+ case 0x800..<0x10000: out.append(UInt8(0xE0 | cp >> 12)); out.append(UInt8(0x80 | (cp >> 6) & 0x3F)); out.append(UInt8(0x80 | cp & 0x3F))
198
+ default: out.append(UInt8(0xF0 | cp >> 18)); out.append(UInt8(0x80 | (cp >> 12) & 0x3F)); out.append(UInt8(0x80 | (cp >> 6) & 0x3F)); out.append(UInt8(0x80 | cp & 0x3F))
199
+ }
200
+ }
201
+ }
202
+ }
swift/Sources/EmbeddingGemma2CoreAI/GemmaTokenizer.swift ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE).
2
+ //
3
+ // Exact re-implementation of the Hugging Face `tokenizers` encode pipeline for the Gemma `tokenizer.json`
4
+ // shipped in this bundle:
5
+ //
6
+ // added tokens leftmost-longest match of the 24 special tokens on the RAW text (they are `normalized: false`)
7
+ // normalizer Replace " " -> "\u{2581}" on every remaining piece
8
+ // pre_tokenizer Split(" ") — a no-op after the normalizer, so each piece is ONE BPE word
9
+ // model BPE, byte_fallback (a scalar missing from the vocab becomes its UTF-8 bytes as `<0xNN>` tokens),
10
+ // merges applied by rank, ties broken by position (the `tokenizers` heap algorithm)
11
+ //
12
+ // Everything operates on Unicode scalars and UTF-8 bytes: no grapheme-cluster (`Character`) iteration and no
13
+ // String equality (which is canonical equivalence in Swift). The vocabulary is keyed by exact UTF-8 bytes, so
14
+ // "é" (U+00E9) and "e" + U+0301 are different keys, as they are for `tokenizers`. tokenizer.json is read with
15
+ // ExactJSON, not Foundation (which strips a leading U+FEFF from string values and corrupts three vocab entries).
16
+ // Verified byte-identical to `tokenizers` 0.23 on tests/tokenizer-cases.json (3,758 texts, 608,225 ids).
17
+
18
+ import Foundation
19
+
20
+ public final class GemmaTokenizer: Sendable {
21
+ public struct LoadError: Error, CustomStringConvertible {
22
+ public let description: String
23
+ }
24
+
25
+ public let bosID: Int32
26
+ public let eosID: Int32
27
+ public let padID: Int32
28
+ public let unkID: Int32
29
+ public let vocabSize: Int
30
+
31
+ private let vocab: [[UInt8]: Int32]
32
+ private let tokenBytes: [[UInt8]] // id -> bytes (for decode)
33
+ private let merges: [UInt64: MergeTarget] // (left id << 32 | right id) -> rank, merged id
34
+ private let byteIDs: [Int32] // 256 entries: id of "<0xNN>"
35
+ private let asciiIDs: [Int32] // 128 entries: id of the 1-byte token, or -1
36
+ private let added: [AddedToken] // longest content first
37
+ private let addedFirstScalars: Set<UInt32>
38
+ private let specialIDs: Set<Int32>
39
+
40
+ private struct MergeTarget { let rank: Int32; let id: Int32 }
41
+ private struct AddedToken { let scalars: [Unicode.Scalar]; let id: Int32 }
42
+
43
+ /// Loads `tokenizer.json`. Throws if the file is not the Gemma BPE configuration this class implements.
44
+ public convenience init(contentsOf url: URL) throws {
45
+ try self.init(data: try Data(contentsOf: url))
46
+ }
47
+
48
+ public init(data: Data) throws {
49
+ let root = try ExactJSON.parse(data)
50
+ // --- configuration guards: refuse anything this implementation does not reproduce exactly ---
51
+ guard let norm = root["normalizer"], norm["type"]?.string == "Replace",
52
+ norm["pattern"]?["String"]?.string == " ", norm["content"]?.stringBytes == Array("\u{2581}".utf8)
53
+ else { throw LoadError(description: "unsupported normalizer (expected Replace \" \" -> U+2581)") }
54
+ guard let pre = root["pre_tokenizer"], pre["type"]?.string == "Split", pre["pattern"]?["String"]?.string == " "
55
+ else { throw LoadError(description: "unsupported pre_tokenizer (expected Split \" \")") }
56
+ guard let model = root["model"], model["type"]?.string == "BPE", model["byte_fallback"]?.bool == true,
57
+ model["continuing_subword_prefix"]?.isNull ?? true, model["end_of_word_suffix"]?.isNull ?? true,
58
+ model["dropout"]?.isNull ?? true, (model["ignore_merges"]?.bool ?? false) == false
59
+ else { throw LoadError(description: "unsupported BPE model configuration") }
60
+ guard let vocabPairs = model["vocab"]?.pairs, let mergeList = model["merges"]?.array else {
61
+ throw LoadError(description: "tokenizer.json: missing vocab/merges")
62
+ }
63
+
64
+ // --- vocab: exact UTF-8 bytes -> id ---
65
+ var vocab = [[UInt8]: Int32](minimumCapacity: vocabPairs.count)
66
+ var maxID: Int32 = -1
67
+ for (key, value) in vocabPairs {
68
+ guard let id = value.int else { throw LoadError(description: "vocab value is not an integer") }
69
+ vocab[key] = Int32(id)
70
+ if Int32(id) > maxID { maxID = Int32(id) }
71
+ }
72
+ guard vocab.count == vocabPairs.count else {
73
+ throw LoadError(description: "duplicate vocab keys (\(vocab.count) != \(vocabPairs.count))")
74
+ }
75
+ var tokenBytes = [[UInt8]](repeating: [], count: Int(maxID) + 1)
76
+ for (bytes, id) in vocab { tokenBytes[Int(id)] = bytes }
77
+
78
+ // --- merges: [a, b] (or "a b") -> rank, id of a+b ---
79
+ var merges = [UInt64: MergeTarget](minimumCapacity: mergeList.count)
80
+ for (rank, m) in mergeList.enumerated() {
81
+ let a: [UInt8], b: [UInt8]
82
+ if let pair = m.array, pair.count == 2, let x = pair[0].stringBytes, let y = pair[1].stringBytes {
83
+ (a, b) = (x, y)
84
+ } else if let s = m.stringBytes, let sp = s.firstIndex(of: 0x20) {
85
+ (a, b) = (Array(s[..<sp]), Array(s[(sp + 1)...]))
86
+ } else {
87
+ throw LoadError(description: "merge #\(rank) has an unexpected shape")
88
+ }
89
+ guard let ia = vocab[a], let ib = vocab[b], let iab = vocab[a + b] else {
90
+ throw LoadError(description: "merge #\(rank) references a token missing from the vocab")
91
+ }
92
+ merges[GemmaTokenizer.pairKey(ia, ib)] = MergeTarget(rank: Int32(rank), id: iab)
93
+ }
94
+
95
+ // --- byte fallback tokens and the ASCII fast path ---
96
+ var byteIDs = [Int32](repeating: -1, count: 256)
97
+ for b in 0..<256 {
98
+ guard let id = vocab[Array(String(format: "<0x%02X>", b).utf8)] else {
99
+ throw LoadError(description: "byte fallback token <0x\(String(format: "%02X", b))> missing")
100
+ }
101
+ byteIDs[b] = id
102
+ }
103
+ var asciiIDs = [Int32](repeating: -1, count: 128)
104
+ for c in 0..<128 { asciiIDs[c] = vocab[[UInt8(c)]] ?? -1 }
105
+
106
+ // --- added (special) tokens: only the exact-match flavour is supported ---
107
+ var added: [AddedToken] = []
108
+ for t in root["added_tokens"]?.array ?? [] {
109
+ guard let content = t["content"]?.stringBytes, let id = t["id"]?.int else { continue }
110
+ let flags = ["normalized", "lstrip", "rstrip", "single_word"].map { t[$0]?.bool ?? false }
111
+ guard !flags.contains(true) else {
112
+ throw LoadError(description: "added token \(String(decoding: content, as: UTF8.self)) uses normalized/lstrip/rstrip/single_word, which is not supported")
113
+ }
114
+ added.append(AddedToken(scalars: Array(String(decoding: content, as: UTF8.self).unicodeScalars), id: Int32(id)))
115
+ }
116
+ added.sort { $0.scalars.count > $1.scalars.count }
117
+
118
+ func special(_ name: String) throws -> Int32 {
119
+ guard let id = vocab[Array(name.utf8)] else { throw LoadError(description: "\(name) missing from vocab") }
120
+ return id
121
+ }
122
+ self.vocab = vocab
123
+ self.tokenBytes = tokenBytes
124
+ self.merges = merges
125
+ self.byteIDs = byteIDs
126
+ self.asciiIDs = asciiIDs
127
+ self.added = added
128
+ self.addedFirstScalars = Set(added.compactMap { $0.scalars.first?.value })
129
+ self.specialIDs = Set(added.map { $0.id })
130
+ self.vocabSize = vocab.count
131
+ self.bosID = try special("<bos>")
132
+ self.eosID = try special("<eos>")
133
+ self.padID = try special("<pad>")
134
+ self.unkID = try special("<unk>")
135
+ }
136
+
137
+ @inline(__always) private static func pairKey(_ a: Int32, _ b: Int32) -> UInt64 {
138
+ (UInt64(UInt32(bitPattern: a)) << 32) | UInt64(UInt32(bitPattern: b))
139
+ }
140
+
141
+ /// Token ids of `text` without `<bos>`/`<eos>` (the same as `tokenizers` `encode(text, add_special_tokens=False)`).
142
+ /// Special tokens written literally in the text (e.g. "<eos>") are recognised, exactly as `tokenizers` does.
143
+ public func encode(_ text: String) -> [Int32] {
144
+ let scalars = Array(text.unicodeScalars)
145
+ var ids: [Int32] = []
146
+ ids.reserveCapacity(scalars.count / 3 + 4)
147
+ var i = 0, pieceStart = 0
148
+ while i < scalars.count {
149
+ if addedFirstScalars.contains(scalars[i].value), let hit = matchAdded(scalars, at: i) {
150
+ if i > pieceStart { bpe(scalars[pieceStart..<i], into: &ids) }
151
+ ids.append(hit.id)
152
+ i += hit.scalars.count
153
+ pieceStart = i
154
+ } else {
155
+ i += 1
156
+ }
157
+ }
158
+ if pieceStart < scalars.count { bpe(scalars[pieceStart...], into: &ids) }
159
+ return ids
160
+ }
161
+
162
+ /// `<bos>` + encode(text) + `<eos>`.
163
+ public func encodeWithSpecials(_ text: String) -> [Int32] {
164
+ [bosID] + encode(text) + [eosID]
165
+ }
166
+
167
+ /// Text for a sequence of ids (the bundle's decoder: "\u{2581}" -> " ", byte-fallback tokens fused into UTF-8).
168
+ public func decode(_ ids: [Int32], skipSpecialTokens: Bool = true) -> String {
169
+ var bytes: [UInt8] = []
170
+ for id in ids {
171
+ guard id >= 0, Int(id) < tokenBytes.count else { continue }
172
+ if skipSpecialTokens, specialIDs.contains(id) { continue }
173
+ if let b = byteIDs.firstIndex(of: id) { bytes.append(UInt8(b)); continue }
174
+ let t = tokenBytes[Int(id)]
175
+ var j = 0
176
+ while j < t.count { // replace every U+2581 (E2 96 81) by a space
177
+ if j + 2 < t.count, t[j] == 0xE2, t[j + 1] == 0x96, t[j + 2] == 0x81 { bytes.append(0x20); j += 3 }
178
+ else { bytes.append(t[j]); j += 1 }
179
+ }
180
+ }
181
+ return String(decoding: bytes, as: UTF8.self)
182
+ }
183
+
184
+ private func matchAdded(_ s: [Unicode.Scalar], at i: Int) -> AddedToken? {
185
+ for t in added where i + t.scalars.count <= s.count { // sorted longest first => first hit is the longest
186
+ var k = 0
187
+ while k < t.scalars.count, s[i + k] == t.scalars[k] { k += 1 }
188
+ if k == t.scalars.count { return t }
189
+ }
190
+ return nil
191
+ }
192
+
193
+ // MARK: - BPE on one word (the `tokenizers` Word::merge_all algorithm)
194
+
195
+ private struct HeapEntry { let rank: Int32; let pos: Int32; let id: Int32 }
196
+
197
+ private func bpe(_ piece: ArraySlice<Unicode.Scalar>, into out: inout [Int32]) {
198
+ // 1. initial symbols: one per scalar (space -> U+2581), byte fallback for scalars missing from the vocab
199
+ var syms: [Int32] = []
200
+ syms.reserveCapacity(piece.count)
201
+ var utf8 = [UInt8](repeating: 0, count: 4)
202
+ for sc in piece {
203
+ let v = sc.value
204
+ if v < 128, v != 0x20 {
205
+ let id = asciiIDs[Int(v)]
206
+ syms.append(id >= 0 ? id : byteIDs[Int(v)])
207
+ continue
208
+ }
209
+ let scalar = v == 0x20 ? Unicode.Scalar(0x2581)! : sc
210
+ var n = 0
211
+ for b in String(scalar).utf8 { utf8[n] = b; n += 1 } // the UTF-8 of ONE scalar: nothing is normalized
212
+ let key = Array(utf8[0..<n])
213
+ if let id = vocab[key] {
214
+ syms.append(id)
215
+ } else {
216
+ for b in key { syms.append(byteIDs[Int(b)]) }
217
+ }
218
+ }
219
+ let n = syms.count
220
+ if n < 2 { out.append(contentsOf: syms); return }
221
+
222
+ // 2. merge by rank, leftmost first among equal ranks (binary heap on (rank, pos))
223
+ var next = [Int32](repeating: 0, count: n), prev = [Int32](repeating: 0, count: n)
224
+ var alive = [Bool](repeating: true, count: n)
225
+ for i in 0..<n { next[i] = Int32(i + 1); prev[i] = Int32(i - 1) }
226
+ var heap: [HeapEntry] = []
227
+ heap.reserveCapacity(n)
228
+ @inline(__always) func less(_ a: HeapEntry, _ b: HeapEntry) -> Bool {
229
+ a.rank != b.rank ? a.rank < b.rank : a.pos < b.pos
230
+ }
231
+ func push(_ e: HeapEntry) {
232
+ heap.append(e)
233
+ var c = heap.count - 1
234
+ while c > 0 {
235
+ let p = (c - 1) >> 1
236
+ if less(heap[c], heap[p]) { heap.swapAt(c, p); c = p } else { break }
237
+ }
238
+ }
239
+ func pop() -> HeapEntry {
240
+ let top = heap[0]
241
+ let last = heap.removeLast()
242
+ if !heap.isEmpty {
243
+ heap[0] = last
244
+ var p = 0
245
+ while true {
246
+ let l = 2 * p + 1, r = l + 1
247
+ var m = p
248
+ if l < heap.count, less(heap[l], heap[m]) { m = l }
249
+ if r < heap.count, less(heap[r], heap[m]) { m = r }
250
+ if m == p { break }
251
+ heap.swapAt(p, m); p = m
252
+ }
253
+ }
254
+ return top
255
+ }
256
+ for i in 0..<(n - 1) {
257
+ if let m = merges[GemmaTokenizer.pairKey(syms[i], syms[i + 1])] {
258
+ push(HeapEntry(rank: m.rank, pos: Int32(i), id: m.id))
259
+ }
260
+ }
261
+ while !heap.isEmpty {
262
+ let e = pop()
263
+ let i = Int(e.pos)
264
+ if !alive[i] { continue }
265
+ let j = Int(next[i])
266
+ if j >= n || !alive[j] { continue }
267
+ guard let m = merges[GemmaTokenizer.pairKey(syms[i], syms[j])], m.rank == e.rank else { continue }
268
+ syms[i] = m.id
269
+ alive[j] = false
270
+ next[i] = next[j]
271
+ if Int(next[i]) < n { prev[Int(next[i])] = Int32(i) }
272
+ if prev[i] >= 0, let m2 = merges[GemmaTokenizer.pairKey(syms[Int(prev[i])], syms[i])] {
273
+ push(HeapEntry(rank: m2.rank, pos: prev[i], id: m2.id))
274
+ }
275
+ if Int(next[i]) < n, let m2 = merges[GemmaTokenizer.pairKey(syms[i], syms[Int(next[i])])] {
276
+ push(HeapEntry(rank: m2.rank, pos: Int32(i), id: m2.id))
277
+ }
278
+ }
279
+ for i in 0..<n where alive[i] { out.append(syms[i]) }
280
+ }
281
+ }
swift/Sources/eg2-cli/main.swift ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE).
2
+ //
3
+ // eg2-cli — command-line front end for the EmbeddingGemma2CoreAI library.
4
+ //
5
+ // eg2-cli embed <bundle> [--compute ane|gpu|cpu] [--kind document|search_query|...] [--dim 768] "text" ...
6
+ // eg2-cli smoke <bundle> <smoke-reference.json> [--compute ane|gpu|cpu] [--cleanup-temp]
7
+ // eg2-cli tokcheck <bundle> <tokenizer-cases.json>
8
+ // eg2-cli latency <bundle> [--compute ane|gpu|cpu] [--trials 50]
9
+ // eg2-cli prewarm <bundle> [--compute ane|gpu|cpu]
10
+ // eg2-cli cleanup-temp (removes this process's MPSGraph temp folders; mostly useful after the others)
11
+ //
12
+ // Every command accepts --cleanup-temp to delete the MPSGraph temp folders this process created, after the
13
+ // model is released (see EmbeddingGemma2.removeOwnMPSGraphTempFiles).
14
+
15
+ import EmbeddingGemma2CoreAI
16
+ import Foundation
17
+
18
+ var args = Array(CommandLine.arguments.dropFirst())
19
+
20
+ @MainActor func usage() -> Never {
21
+ print("""
22
+ usage: eg2-cli <embed|smoke|tokcheck|latency|prewarm|cleanup-temp> <bundle> [...] [--compute ane|gpu|cpu] [--cleanup-temp]
23
+ """)
24
+ exit(2)
25
+ }
26
+
27
+ @MainActor func takeOption(_ name: String) -> String? {
28
+ guard let i = args.firstIndex(of: name), i + 1 < args.count else { return nil }
29
+ let v = args[i + 1]
30
+ args.removeSubrange(i...(i + 1))
31
+ return v
32
+ }
33
+
34
+ @MainActor func takeFlag(_ name: String) -> Bool {
35
+ guard let i = args.firstIndex(of: name) else { return false }
36
+ args.remove(at: i)
37
+ return true
38
+ }
39
+
40
+ func fmt(_ x: Double, _ digits: Int = 2) -> String { String(format: "%.\(digits)f", x) }
41
+
42
+ func cpuSeconds() -> Double {
43
+ var u = rusage()
44
+ getrusage(RUSAGE_SELF, &u)
45
+ return Double(u.ru_utime.tv_sec) + Double(u.ru_utime.tv_usec) / 1e6 + Double(u.ru_stime.tv_sec) + Double(u.ru_stime.tv_usec) / 1e6
46
+ }
47
+
48
+ /// Peak physical footprint (MB) and the Neural Engine footprint (MB) of this process (proc_pid_rusage, RUSAGE_INFO_V6).
49
+ func footprints() -> (physMB: Double, neuralMB: Double) {
50
+ var r = rusage_info_v6()
51
+ let kr = withUnsafeMutablePointer(to: &r) { p -> Int32 in
52
+ p.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { proc_pid_rusage(getpid(), RUSAGE_INFO_V6, $0) }
53
+ }
54
+ guard kr == 0 else { return (-1, -1) }
55
+ return (Double(r.ri_lifetime_max_phys_footprint) / 1048576, Double(r.ri_neural_footprint) / 1048576)
56
+ }
57
+
58
+ guard !args.isEmpty else { usage() }
59
+ let cmd = args.removeFirst()
60
+ let cleanup = takeFlag("--cleanup-temp")
61
+ let compute = ComputePreference(rawValue: takeOption("--compute") ?? "ane") ?? .neuralEngine
62
+
63
+ @MainActor func finish(_ code: Int32) -> Never {
64
+ if cleanup {
65
+ let r = EmbeddingGemma2.removeOwnMPSGraphTempFiles()
66
+ print("cleanup-temp: removed \(r.folders) MPSGraph temp folder(s), \(fmt(Double(r.bytes) / 1048576, 1)) MB")
67
+ }
68
+ exit(code)
69
+ }
70
+
71
+ if cmd == "cleanup-temp" { finish(0) }
72
+ guard let bundlePath = args.first else { usage() }
73
+ args.removeFirst()
74
+ let bundle = URL(fileURLWithPath: bundlePath)
75
+
76
+ do {
77
+ switch cmd {
78
+ case "embed":
79
+ let kind = takeOption("--kind") ?? "document"
80
+ let dim = Int(takeOption("--dim") ?? "768") ?? 768
81
+ let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute)
82
+ for text in args {
83
+ let v = try await eg2.embed(text, kind: kind == "none" ? nil : kind, dim: dim)
84
+ let json: [String: Any] = ["text": text, "kind": kind, "dim": dim, "embedding": v.map { Double($0) }]
85
+ print(String(decoding: try JSONSerialization.data(withJSONObject: json), as: UTF8.self))
86
+ }
87
+ _ = consume eg2
88
+ finish(0)
89
+
90
+ case "smoke":
91
+ guard let refPath = args.first else { usage() }
92
+ struct Ref: Decodable { let text: String; let kind: String; let embedding: [Float] }
93
+ struct RefFile: Decodable { let threshold_cosine: Double; let cases: [Ref] }
94
+ let ref = try JSONDecoder().decode(RefFile.self, from: Data(contentsOf: URL(fileURLWithPath: refPath)))
95
+ let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute)
96
+ print("loaded in \(fmt(eg2.loadSeconds, 1)) s (tokenizer \(fmt(eg2.tokenizerLoadSeconds, 2)) s), compute=\(compute.rawValue), buckets=\(eg2.buckets)")
97
+ var ok = true
98
+ for c in ref.cases {
99
+ let v = try await eg2.embed(c.text, kind: c.kind)
100
+ let cos = cosineSimilarity(v, c.embedding)
101
+ let pass = cos >= ref.threshold_cosine && v.allSatisfy { $0.isFinite }
102
+ ok = ok && pass
103
+ print("\(pass ? "PASS" : "FAIL") cosine \(fmt(cos, 6)) (threshold \(ref.threshold_cosine)) \(c.kind): \(c.text.prefix(60))")
104
+ }
105
+ let fp = footprints()
106
+ print("peak footprint \(fmt(fp.physMB, 0)) MB, neural footprint \(fmt(fp.neuralMB, 0)) MB")
107
+ print(ok ? "SMOKE PASS" : "SMOKE FAIL")
108
+ _ = consume eg2
109
+ finish(ok ? 0 : 1)
110
+
111
+ case "tokcheck":
112
+ guard let casesPath = args.first else { usage() }
113
+ struct Case: Decodable { let cat: String; let kind: String; let text: String; let ids: [Int32] }
114
+ let cases = try JSONDecoder().decode([Case].self, from: Data(contentsOf: URL(fileURLWithPath: casesPath)))
115
+ let config = try JSONDecoder().decode(BundleConfig.self, from: Data(contentsOf: bundle.appendingPathComponent("config.json")))
116
+ let t0 = Date()
117
+ let tok = try GemmaTokenizer(contentsOf: bundle.appendingPathComponent("tokenizer/tokenizer.json"))
118
+ print("tokenizer loaded in \(fmt(Date().timeIntervalSince(t0), 2)) s, vocab \(tok.vocabSize)")
119
+ let prefixes = ["q": "task: search result | query: ", "d": "title: none | text: ", "raw": ""]
120
+ var same = 0, totalIDs = 0, shown = 0
121
+ var perCat: [String: (Int, Int)] = [:]
122
+ let t1 = Date()
123
+ let cpu0 = cpuSeconds()
124
+ for c in cases {
125
+ let ids = tok.encodeWithSpecials(prefixes[c.kind]! + c.text)
126
+ let ok = ids == c.ids
127
+ totalIDs += c.ids.count
128
+ var e = perCat[c.cat] ?? (0, 0)
129
+ e.1 += 1
130
+ if ok {
131
+ same += 1; e.0 += 1
132
+ } else if shown < 10 {
133
+ shown += 1
134
+ print("DIFFERENT [\(c.cat)] \(c.text.prefix(60).debugDescription): got \(ids.prefix(12)) expected \(c.ids.prefix(12))")
135
+ }
136
+ perCat[c.cat] = e
137
+ }
138
+ let secs = Date().timeIntervalSince(t1), cpu = cpuSeconds() - cpu0
139
+ for (cat, e) in perCat.sorted(by: { $0.key < $1.key }) { print(" \(cat): \(e.0)/\(e.1)") }
140
+ print("TOKCHECK \(same)/\(cases.count) texts identical to Hugging Face tokenizers (\(totalIDs) ids) in \(fmt(secs, 2)) s wall / \(fmt(cpu, 2)) s CPU")
141
+ print("config bos/eos/pad \(config.bos_token_id)/\(config.eos_token_id)/\(config.pad_token_id) == tokenizer \(tok.bosID)/\(tok.eosID)/\(tok.padID)")
142
+ finish(same == cases.count ? 0 : 1)
143
+
144
+ case "latency":
145
+ let trials = Int(takeOption("--trials") ?? "50") ?? 50
146
+ let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute)
147
+ print("loaded in \(fmt(eg2.loadSeconds, 1)) s, compute=\(compute.rawValue)")
148
+ let warm = try await eg2.prewarm()
149
+ print("prewarm (one call per bucket) \(fmt(warm, 2)) s")
150
+ print("bucket p50 ms min ms p90 ms CPU ms/call tok/s")
151
+ for S in eg2.buckets {
152
+ var ids = [Int32](repeating: 0, count: S)
153
+ ids[0] = eg2.config.bos_token_id; ids[S - 1] = eg2.config.eos_token_id
154
+ for i in 1..<(S - 1) { ids[i] = Int32(1000 + (i * 7919) % 200000) }
155
+ for _ in 0..<3 { _ = try await eg2.embed(ids: ids, bucket: S) }
156
+ var ts: [Double] = []
157
+ let cpu0 = cpuSeconds()
158
+ for _ in 0..<trials {
159
+ let t = Date()
160
+ _ = try await eg2.embed(ids: ids, bucket: S)
161
+ ts.append(Date().timeIntervalSince(t) * 1000)
162
+ }
163
+ let cpu = (cpuSeconds() - cpu0) / Double(trials) * 1000
164
+ ts.sort()
165
+ let p50 = ts[ts.count / 2], p90 = ts[min(ts.count - 1, Int(Double(ts.count) * 0.9))]
166
+ print(String(format: "%5d %7.2f %7.2f %7.2f %11.2f %6.0f", S, p50, ts[0], p90, cpu, Double(S) / p50 * 1000))
167
+ }
168
+ let fp = footprints()
169
+ print("peak footprint \(fmt(fp.physMB, 0)) MB, neural footprint \(fmt(fp.neuralMB, 0)) MB (\(fp.neuralMB > 0 ? "Neural Engine in use" : "Neural Engine NOT used"))")
170
+ _ = consume eg2
171
+ finish(0)
172
+
173
+ case "prewarm":
174
+ let t0 = Date()
175
+ let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute)
176
+ let warm = try await eg2.prewarm()
177
+ print("load \(fmt(eg2.loadSeconds, 1)) s + prewarm \(fmt(warm, 2)) s = ready in \(fmt(Date().timeIntervalSince(t0), 1)) s")
178
+ _ = consume eg2
179
+ finish(0)
180
+
181
+ default:
182
+ usage()
183
+ }
184
+ } catch {
185
+ print("error: \(error)")
186
+ finish(1)
187
+ }
tests/make_smoke_reference.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
3
+ """Regenerate `smoke-reference.json`: fp32 reference embeddings for three fictional sentences, computed with
4
+ sentence-transformers on the ORIGINAL google/embeddinggemma-2 checkpoint (text only, CPU, float32).
5
+
6
+ python3 make_smoke_reference.py --model google/embeddinggemma-2 --out smoke-reference.json
7
+
8
+ Needs: sentence-transformers >= 6.1, transformers >= 5.19, torch, torchvision and pillow (the checkpoint's processor
9
+ is multimodal and imports them even for text). The shipped file was produced with sentence-transformers 6.1.0 /
10
+ transformers 5.19.0 / torch 2.14.1; torchvision 0.29.1 is the matching version (see ../python/requirements.lock).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ from pathlib import Path
17
+
18
+ CASES = [
19
+ {"kind": "search_query", "text": "When does the warehouse lease in Brindlemoor expire?"},
20
+ {"kind": "document", "text": "The warehouse lease in Brindlemoor runs until the last day of March; the landlord agreed to fix the window before then."},
21
+ {"kind": "document", "text": "Lista della spesa: latte d'avena, due limoni, riso basmati, coriandolo e croccantini per il cane."},
22
+ ]
23
+ PROMPT_NAME = {"search_query": "query", "document": "document"}
24
+
25
+
26
+ def main() -> None:
27
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
28
+ ap.add_argument("--model", default="google/embeddinggemma-2", help="HF id or local checkpoint folder")
29
+ ap.add_argument("--out", default="smoke-reference.json")
30
+ ap.add_argument("--threshold", type=float, default=0.999)
31
+ args = ap.parse_args()
32
+ import sentence_transformers
33
+ import torch
34
+ import transformers
35
+ from sentence_transformers import SentenceTransformer
36
+
37
+ model = SentenceTransformer(args.model, device="cpu", model_kwargs={"torch_dtype": torch.float32})
38
+ out = {
39
+ "reference": "sentence-transformers fp32 on CPU, original checkpoint, text only",
40
+ "versions": {"sentence_transformers": sentence_transformers.__version__, "transformers": transformers.__version__,
41
+ "torch": torch.__version__},
42
+ "threshold_cosine": args.threshold,
43
+ "cases": [],
44
+ }
45
+ for c in CASES:
46
+ v = model.encode(c["text"], prompt_name=PROMPT_NAME[c["kind"]], normalize_embeddings=True, convert_to_numpy=True)
47
+ out["cases"].append({**c, "embedding": [round(float(x), 7) for x in v.tolist()]})
48
+ print(c["kind"], len(v), c["text"][:50])
49
+ Path(args.out).write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
50
+ print("wrote", args.out)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
tests/make_tokenizer_cases.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
3
+ """Generate the tokenizer conformance fixture `tokenizer-cases.json`: >= 3,000 deterministic, FICTIONAL texts that
4
+ stress the Gemma tokenizer (CRLF, NFD accents, emoji ZWJ / skin tones / flags / keycaps, Arabic with diacritics, Thai,
5
+ Hindi, CJK, whitespace runs > 31, tabs, control characters, special-token look-alikes, very long inputs), with the ids
6
+ produced by the Hugging Face `tokenizers` library for `<bos> + encode(prefix + text) + <eos>` (no truncation).
7
+
8
+ python3 make_tokenizer_cases.py --tokenizer ../model/tokenizer/tokenizer.json --out tokenizer-cases.json
9
+
10
+ The people, companies, events and numbers below are invented. Most place names are invented too; a few real
11
+ place names and words appear in the accent and script stress strings (e.g. Łódź, Đà Nẵng, Ångström).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import random
18
+ import unicodedata
19
+ from pathlib import Path
20
+
21
+ PREFIX = {"q": "task: search result | query: ", "d": "title: none | text: ", "raw": ""}
22
+
23
+ FIRST = ["Orla", "Teodor", "Mirela", "Kasimir", "Ysolde", "Bram", "Fenna", "Ruaridh", "Ottilie", "Severin", "Jolan",
24
+ "Liesl", "Corvin", "Tamsin", "Ilse", "Quillon", "Saskia", "Elowen", "Thaddeus", "Perpetua"]
25
+ LAST = ["Vantrell", "Dunholm", "Brecklow", "Szarvas", "Penhallow", "Lindqvar", "Castelmoro", "Ravensby", "Quarterman",
26
+ "Halloran-Bey", "Møllergaard", "Åkerlund", "Nowakówna", "Çelikkaya", "Đặng", "O'Rourke-Vale"]
27
+ ORGS = ["Lumenvale", "Brightwater Mills", "Okkam Freight", "Velmora Labs", "Quillfeather", "Saltmarsh & Co",
28
+ "Norrow Analytics", "Cobaltine", "Haverly Bikes", "Studio Morello", "Tavola Verde", "Pinecrest Dental"]
29
+ TOWNS = ["Brindlemoor", "Castel Ferrino", "Ostwick", "Valdorna", "Pellham Cross", "Marrowgate", "San Lucetto",
30
+ "Hadleigh Fen", "Rocca Bruna", "Thistleby", "Søndervik", "Łęczyca Nowa", "Çamlıdere", "Žďár Horní"]
31
+ THINGS = ["the quarterly invoice", "the boiler service", "the onboarding checklist", "the API rate limits",
32
+ "the warehouse lease", "the beta feedback", "the dentist appointment", "the hiring plan", "the tax return",
33
+ "the server migration", "the book club", "the roof estimate", "the ferry timetable"]
34
+ COSE = ["la fattura trimestrale", "la revisione della caldaia", "il contratto d'affitto", "il preventivo del tetto",
35
+ "la riunione di condominio", "il volo per Valdorna", "la visita dal dentista", "il piano ferie",
36
+ "il trasloco di sabato", "il corso di ceramica"]
37
+ DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
38
+ MONTHS = ["January", "March", "May", "July", "September", "October", "November", "December"]
39
+
40
+ EN_TPL = [
41
+ "{p} from {o} moved {t} to {d} because the team in {town} is short-staffed this month.",
42
+ "The budget for {t} is {n},{m:03d} and {p} wants a second quote before {d}.",
43
+ "Remember to call {p} about {t}; the office in {town} closes at {h}:30.",
44
+ "{o} signed the renewal: {n} seats at {m} per seat, billed every {mo}.",
45
+ "On {d} the deploy failed twice because a config flag was read as seconds instead of milliseconds.",
46
+ "Minutes: {p} presented the roadmap, {p2} raised concerns about {t}, and the decision was postponed to {mo}.",
47
+ "Our p99 latency rose to {m} ms after the {d} release; rollback is ready if it passes {h}0 ms.",
48
+ "Grocery list: oat milk, two lemons, basmati rice, coriander, and dog treats for Biscuit.",
49
+ "Flight {o2}{n} to {town} departs at {h}:{m2:02d}; seat {m2}C, one checked bag.",
50
+ "Draft reply to {p}: thanks for the notes on {t}, we will send the revised version by {d}.",
51
+ "The landlord in {town} agreed to fix the window before {mo}; the rent stays at {n},{m:03d} a year.",
52
+ ]
53
+ IT_TPL = [
54
+ "{p} di {o} ha spostato {c} a {d} perché l'ufficio di {town} è sotto organico.",
55
+ "Il preventivo per {c} è di {n}.{m:03d} euro e {p} vuole un secondo parere entro {d}.",
56
+ "Ricordati di chiamare {p} per {c}: lo studio di {town} chiude alle {h}:30.",
57
+ "Verbale: {p} ha presentato il piano, {p2} ha sollevato dubbi su {c} e la decisione slitta a {mo}.",
58
+ "La caldaia perde ancora; il tecnico dice che la valvola costa {m} euro e serve una settimana.",
59
+ "Lista della spesa: latte d'avena, due limoni, riso basmati, coriandolo e croccantini per il cane.",
60
+ "Il volo per {town} parte alle {h}:{m2:02d}; posto {m2}C, un bagaglio in stiva. Perché così presto?",
61
+ ]
62
+ OTHER = {
63
+ "de": ["Die Heizung in {town} fällt seit {d} ständig aus; {p} ruft morgen den Installateur an.",
64
+ "Größere Änderungen am Vertrag mit {o} müssen bis {mo} unterschrieben werden."],
65
+ "fr": ["{p} a déplacé la réunion à {h} h parce que le train pour {town} était en retard.",
66
+ "Le devis de {o} s'élève à {n} {m:03d} € ; l'échéance est fixée à {mo}."],
67
+ "es": ["¿Quién se encarga de {t} en {o}? {p} dijo que el plazo termina el {d}.",
68
+ "El alquiler en {town} sube a {n}.{m:03d} al año; ¡hay que renegociar antes de {mo}!"],
69
+ "pt": ["{p} avisou que a entrega de {o} chega a {town} na {d}, por volta das {h}h.",
70
+ "O orçamento para {t} ficou em {n}.{m:03d}; a decisão foi adiada para {mo}."],
71
+ "nl": ["{p} belt {d} met {o} over {t}; het kantoor in {town} sluit om {h}:30.",
72
+ "De huur in {town} blijft {n}.{m:03d} per jaar als het raam vóór {mo} gemaakt is."],
73
+ "pl": ["{p} przeniósł spotkanie na {d}, bo zespół w {town} ma za mało ludzi.",
74
+ "Faktura od {o} opiewa na {n} {m:03d} zł; termin płatności mija w {mo}."],
75
+ "tr": ["{p}, {o} ile yapılan sözleşmeyi {mo} ayına kadar imzalamalı; ofis {h}:30'da kapanıyor.",
76
+ "{town}'daki kira yılda {n}.{m:03d} olarak kalıyor; pencere {mo} öncesi tamir edilecek."],
77
+ "vi": ["{p} đã dời cuộc họp sang {d} vì đội ở {town} đang thiếu người.",
78
+ "Hóa đơn của {o} là {n}.{m:03d}; hạn thanh toán là tháng {m2}."],
79
+ "ru": ["{p} перенёс встречу на {d}, потому что в {town} не хватает людей.",
80
+ "Счёт от {o} на {n} {m:03d}; оплатить нужно до {mo}."],
81
+ "el": ["Ο {p} μετέφερε τη συνάντηση την {d} επειδή η ομάδα στο {town} είναι λίγη.",
82
+ "Το τιμολόγιο της {o} είναι {n}.{m:03d}· πληρωμή μέχρι {mo}."],
83
+ "he": ["{p} הזיז את הפגישה ל-{d} כי הצוות ב-{town} קטן מדי.",
84
+ "החשבונית מ-{o} היא {n},{m:03d}; לשלם עד {mo}."],
85
+ "ar": ["نَقَلَ {p} الاجْتِماعَ إلى يَوْمِ {d} لِأَنَّ الفَريقَ في {town} قَليلُ العَدَدِ هذا الشَّهْرَ.",
86
+ "الفاتُورَةُ مِنْ {o} بِقيمَةِ {n}٬{m:03d}؛ يَجِبُ الدَّفْعُ قَبْلَ {mo}.",
87
+ "تَذَكَّرْ أَنْ تَتَّصِلَ بِـ{p} بِخُصُوصِ {t}؛ المَكْتَبُ يُغْلِقُ السّاعَةَ {h}:30."],
88
+ "fa": ["{p} جلسه را به {d} موکول کرد چون تیم {town} این ماه کم‌نیرو است.",
89
+ "صورت‌حساب {o} به مبلغ {n}٬{m:03d} است؛ پرداخت تا {mo}."],
90
+ "th": ["{p} เลื่อนการประชุมไปวัน{d} เพราะทีมที่{town}มีคนไม่พอในเดือนนี้",
91
+ "ใบแจ้งหนี้จาก {o} เป็นเงิน {n},{m:03d} บาท ต้องชำระก่อน{mo}",
92
+ "อย่าลืมโทรหา{p}เรื่อง{t} สำนักงานปิดเวลา {h}:30 น."],
93
+ "hi": ["{p} ने बैठक {d} को टाल दी क्योंकि {town} की टीम में इस महीने लोग कम हैं।",
94
+ "{o} का चालान {n},{m:03d} रुपये का है; भुगतान {mo} से पहले करना है।",
95
+ "{p} को {t} के बारे में फ़ोन करना याद रखें; दफ़्तर {h}:30 बजे बंद होता है।"],
96
+ "bn": ["{p} সভাটি {d}-এ পিছিয়ে দিয়েছেন কারণ {town}-এর দলে এই মাসে লোক কম।",
97
+ "{o}-এর চালান {n},{m:03d} টাকার; {mo}-এর আগে পরিশোধ করতে হবে।"],
98
+ "ta": ["{p} கூட்டத்தை {d} அன்று ஒத்திவைத்தார்; {town} குழுவில் இந்த மாதம் ஆட்கள் குறைவு.",
99
+ "{o} விலைப்பட்டியல் {n},{m:03d}; {mo}க்கு முன் செலுத்த வேண்டும்."],
100
+ "zh": ["{p}把会议改到了{d},因为{town}的团队这个月人手不够。",
101
+ "{o}的发票金额是{n},{m:03d}元,必须在{mo}之前付款。",
102
+ "记得给{p}打电话谈{t};办公室{h}点半关门。"],
103
+ "zh_tw": ["{p}把會議改到了{d},因為{town}的團隊這個月人手不夠。",
104
+ "{o}的發票金額是{n},{m:03d}元,必須在{mo}之前付款。"],
105
+ "ja": ["{p}さんは{town}のチームが今月人手不足なので、会議を{d}に延期しました。",
106
+ "{o}からの請求書は{n},{m:03d}円で、{mo}までに支払う必要があります。",
107
+ "{t}について{p}さんに電話するのを忘れないで。オフィスは{h}時半に閉まります。"],
108
+ "ko": ["{p}은(는) {town} 팀이 이번 달 인력이 부족해서 회의를 {d}로 미뤘습니다.",
109
+ "{o}의 청구서는 {n},{m:03d}원이며 {mo} 전에 결제해야 합니다.",
110
+ "{t} 건으로 {p}에게 전화하는 것 잊지 마세요. 사무실은 {h}시 30분에 닫습니다."],
111
+ "ka": ["{p}-მ��� შეხვედრა {d}-ზე გადადო, რადგან {town}-ის გუნდი ამ თვეში მცირეა."],
112
+ "hy": ["{p}-ը հանդիպումը տեղափոխեց {d}, քանի որ {town}-ի թիմը այս ամիս փոքր է։"],
113
+ "am": ["{p} ስብሰባውን ወደ {d} አዛወረው፣ ምክንያቱም በ{town} ያለው ቡድን በዚህ ወር ትንሽ ነው።"],
114
+ "uk": ["{p} переніс зустріч на {d}, бо команді в {town} бракує людей."],
115
+ "cs": ["{p} přesunul schůzku na {d}, protože tým v {town} má tento měsíc málo lidí."],
116
+ "fi": ["{p} siirsi kokouksen {d}, koska {town}n tiimi on tässä kuussa alimiehitetty."],
117
+ "hu": ["{p} áthelyezte a megbeszélést {d}-ra, mert a {town}-i csapat ebben a hónapban kevés."],
118
+ "sw": ["{p} alihamisha mkutano hadi {d} kwa sababu timu ya {town} ina watu wachache mwezi huu."],
119
+ "id": ["{p} memindahkan rapat ke {d} karena tim di {town} kekurangan orang bulan ini."],
120
+ }
121
+ QUERIES = ["When is {t} due?", "Who handles {t} at {o}?", "How much did {o} quote for {t}?",
122
+ "What did {p} decide about the {town} office?", "Which flight goes to {town} on {d}?",
123
+ "Why was the deploy rolled back?", "What is the rent in {town}?", "Quando scade {c}?",
124
+ "Chi si occupa di {c} a {town}?", "¿Cuánto cuesta {t}?", "Wann endet die Testphase von {o}?",
125
+ "{town} 사무실 임대료는 얼마인가요?", "{o}の請求書はいつまで?", "{p}是谁?", "ما هو إيجار {town}؟",
126
+ "{town}में किराया कितना है?", "ค่าเช่าใน{town}เท่าไหร่", "p99 latency", "SOC 2", "INC-{m}", "wifi password"]
127
+ CODE = [
128
+ "def backoff(attempt: int, base: float = 0.5, cap: float = {m}.0) -> float:\n return min(cap, base * 2 ** attempt)\n",
129
+ "func retryDelay(_ attempt: Int) -> Duration {{\n\t.milliseconds(min({m}_000, 500 << attempt))\n}}\n",
130
+ "SELECT customer_id, SUM(amount) AS total\nFROM invoices\nWHERE issued_at >= '2026-{m2:02d}-01'\nGROUP BY customer_id\nHAVING SUM(amount) > {n}{m};\n",
131
+ "const queue = new Map();\nexport function enqueue(id, payload) {{\n if (queue.size > {m}) throw new Error('queue full');\n queue.set(id, {{ payload, tries: 0 }});\n}}\n",
132
+ "#!/bin/sh\nset -eu\nfor f in logs/*.gz; do\n zcat \"$f\" | grep -c 'status={h}0' || true\ndone\n",
133
+ "{{\n \"service\": \"{o2}-webhooks\",\n \"region\": \"ams-1\",\n \"retries\": {h},\n \"timeout_ms\": {m}0\n}}\n",
134
+ "class DeliveryQueue:\n def __init__(self, limit={m}):\n self.items = []\n\n def push(self, item):\n if len(self.items) >= self.limit:\n raise OverflowError(item)\n",
135
+ "fn parse_port(s: &str) -> Result<u16, String> {{\n s.trim().parse::<u16>().map_err(|e| format!(\"bad port {{s}}: {{e}}\"))\n}}\n",
136
+ "<div class=\"card\">\r\n\t<span id=\"x{m}\">&nbsp;</span>\r\n</div>\r\n",
137
+ "x = [i**2 for i in range({m}) if i % {h} == 0] # noqa\n\n\n\n",
138
+ ]
139
+ EMOJI = ["👩‍💻", "👨‍👩‍👧‍👦", "🏳️‍🌈", "🏴‍☠️", "👍🏽", "🧑🏿‍🚀", "🇮🇹", "🇯🇵", "🇺🇳", "1️⃣", "#️⃣", "*️⃣", "❤️", "❤", "🫶🏼",
140
+ "👩🏻‍❤️‍💋‍👨🏾", "🏃‍♀️", "🧘🏽‍♂️", "🫠", "🐈‍⬛", "🐻‍❄️", "😶‍🌫️", "🇿🇿", "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "✌🏻", "☝🏿", "🅰️", "™", "©️", "⚠️"]
141
+ LOOKALIKES = ["<bos>", "<eos>", "<pad>", "<unk>", "<mask>", "<|tool>", "<tool|>", "<|tool_call>", "<tool_call|>",
142
+ "<|tool_response>", "<tool_response|>", "<|\"|>", "<|think|>", "<|channel>", "<channel|>", "<|turn>",
143
+ "<turn|>", "<|image>", "<|audio>", "<|image|>", "<|audio|>", "<image|>", "<audio|>", "<|video|>",
144
+ "<start_of_turn>", "<end_of_turn>", "<bos", "bos>", "< bos >", "<BOS>", "<Bos>", "<eos><bos>", "<eos><eos>",
145
+ "<|tool><tool|>", "<|tool", "<|tool>>", "<<bos>>", "<bos>text<eos>", "&lt;bos&gt;", "<0x0A>", "<0xFF>",
146
+ "<unused0>", "[multimodal]", "<|im_start|>", "<s>", "</s>", "<|endoftext|>", "▁", "▁▁", "▁<bos>▁", "<▁bos>"]
147
+ CONTROLS = ["\x00", "\x01", "\x07", "\x08", "\x0b", "\x0c", "\x1b", "\x1f", "\x7f", "\x85", "\xa0", " ", " ",
148
+ " ", "​", "‌", "‍", "‎", "‏", "
", "
", "‪", "‮", "⁠",
149
+ "⁦", "⁩", " ", "", "￾", "￿", "�", "", "\U000e0001", "\U0010ffff",
150
+ "\U0001d11e", "\U00020000", "\U0002a6d6", "\U0003134a"]
151
+ ACCENTED = ["crème brûlée à la façon de {town}", "Ñandú y pingüino en el Río Zañartú", "Søren Ångström, Þórður Ægir",
152
+ "Łódź, Žilina, Ștefan, Țara", "ẞtraße, naïve, coöperate, résumé, exposé", "Việt Nam, Hồ Chí Minh? Đà Nẵng!",
153
+ "Džemal, Ljubljana, Njegoš", "Ḥamid, Ṣalāḥ, Ṭāriq, Ẓāhir", "Ễ ô̂ ạ̈ ṓ ṩ ǟ", "Ά έ ή ί ό ύ ώ ϊ ϋ ΐ ΰ"]
154
+ WS = [" ", "\n", "\t", "\r\n", "\r", " \t", " ", "\xa0", "\n\n"]
155
+
156
+
157
+ def fill(tpl: str, r: random.Random) -> str:
158
+ o = r.choice(ORGS)
159
+ return tpl.format(p=f"{r.choice(FIRST)} {r.choice(LAST)}", p2=r.choice(FIRST), o=o, o2=o.split()[0].lower()[:6],
160
+ t=r.choice(THINGS), c=r.choice(COSE), d=r.choice(DAYS), mo=r.choice(MONTHS), town=r.choice(TOWNS),
161
+ n=r.randint(2, 98), m=r.randint(10, 999), m2=r.randint(1, 59), h=r.randint(7, 21))
162
+
163
+
164
+ def sentence(r: random.Random, langs=None) -> str:
165
+ langs = langs or ["en", "en", "it"] + list(OTHER)
166
+ lang = r.choice(langs)
167
+ pool = {"en": EN_TPL, "it": IT_TPL}.get(lang) or OTHER[lang]
168
+ return fill(r.choice(pool), r)
169
+
170
+
171
+ def rand_scalar(r: random.Random) -> str:
172
+ """A random assigned-or-not scalar from a spread of blocks (never a surrogate)."""
173
+ ranges = [(0x20, 0x7e), (0xa0, 0x24f), (0x370, 0x3ff), (0x400, 0x4ff), (0x590, 0x6ff), (0x900, 0x97f),
174
+ (0xe00, 0xe7f), (0x1100, 0x11ff), (0x2000, 0x206f), (0x2190, 0x23ff), (0x2500, 0x27bf),
175
+ (0x3000, 0x30ff), (0x4e00, 0x9fff), (0xac00, 0xd7a3), (0xe000, 0xf8ff), (0xfb00, 0xfeff),
176
+ (0x10000, 0x1049f), (0x1f300, 0x1faff), (0x20000, 0x2a6df), (0xe0000, 0xe007f), (0x30000, 0x3134f),
177
+ (0xf0000, 0xffffd), (0x100000, 0x10ffff)]
178
+ lo, hi = r.choice(ranges)
179
+ return chr(r.randint(lo, hi))
180
+
181
+
182
+ def build(r: random.Random) -> list[dict]:
183
+ out: list[dict] = []
184
+
185
+ def add(cat, text, kind=None):
186
+ out.append({"cat": cat, "kind": kind or r.choice(["d", "q", "raw"]), "text": text})
187
+
188
+ for _ in range(420):
189
+ add("en", " ".join(sentence(r, ["en"]) for _ in range(r.randint(1, 4))), "d")
190
+ for _ in range(220):
191
+ add("it", " ".join(sentence(r, ["it"]) for _ in range(r.randint(1, 3))), "d")
192
+ for lang in OTHER:
193
+ for _ in range(14 if lang in ("ar", "th", "hi", "zh", "ja", "ko") else 7):
194
+ add(lang, " ".join(sentence(r, [lang]) for _ in range(r.randint(1, 3))))
195
+ for _ in range(300):
196
+ add("query", fill(r.choice(QUERIES), r), "q")
197
+ for _ in range(200):
198
+ add("code", "".join(fill(r.choice(CODE), r) for _ in range(r.randint(1, 3))), "d")
199
+ # CRLF and mixed line endings
200
+ for _ in range(160):
201
+ s = "\r\n".join(sentence(r) for _ in range(r.randint(1, 5)))
202
+ s = r.choice([s, s + "\r\n", "\r\n" + s, s.replace("\r\n", "\r", 1), s.replace("\r\n", "\n\r", 1), s + "\r"])
203
+ add("crlf", s)
204
+ # NFD / NFKC / mixed normalization forms
205
+ for _ in range(160):
206
+ base = fill(r.choice(ACCENTED), r) + " " + sentence(r, ["it", "fr", "pl", "vi", "tr", "cs", "hu", "el", "ru"])
207
+ form = r.choice(["NFD", "NFD", "NFKD", "NFKC", "NFC"])
208
+ s = unicodedata.normalize(form, base)
209
+ if r.random() < 0.4: # mix NFC and NFD halves
210
+ s = unicodedata.normalize("NFC", base[: len(base) // 2]) + unicodedata.normalize("NFD", base[len(base) // 2:])
211
+ add("nfd", s)
212
+ # emoji sequences
213
+ for _ in range(220):
214
+ parts = [r.choice(EMOJI) for _ in range(r.randint(1, 8))]
215
+ words = sentence(r, ["en"]).split()
216
+ s = " ".join(w + (r.choice(parts) if r.random() < 0.3 else "") for w in words) if r.random() < 0.5 else "".join(parts)
217
+ add("emoji", s)
218
+ # scripts with combining marks and no spaces
219
+ for lang, n in (("ar", 100), ("fa", 40), ("he", 30), ("th", 110), ("hi", 100), ("bn", 40), ("ta", 40), ("zh", 70),
220
+ ("zh_tw", 30), ("ja", 70), ("ko", 70), ("ka", 20), ("hy", 20), ("am", 20)):
221
+ for _ in range(n):
222
+ s = sentence(r, [lang])
223
+ if lang in ("ar", "fa") and r.random() < 0.5:
224
+ s = "".join(c + (r.choice("ًٌٍَُِّْ") if r.random() < 0.3 and "ء" <= c <= "ي" else "") for c in s)
225
+ if r.random() < 0.3:
226
+ s = s + " " + sentence(r, ["en"])
227
+ add(lang + "_x", s)
228
+ # whitespace runs > 31, tabs, newline runs, mixed
229
+ for _ in range(180):
230
+ ws = r.choice(WS)
231
+ k = r.choice([31, 32, 33, 40, 63, 64, 65, 100, 127, 128, 129, 200, 500])
232
+ a, b = sentence(r, ["en", "it"]), sentence(r, ["en"])
233
+ s = r.choice([a + ws * k + b, ws * k + a, a + ws * k, ws * k, a + " " * k + "\t" * (k // 3) + b, (ws * k + a) * 2])
234
+ add("whitespace", s)
235
+ for _ in range(100):
236
+ s = "\t".join(sentence(r).split(" ")[: r.randint(1, 12)])
237
+ s = r.choice([s, "\t" + s, s + "\t\t", s.replace("\t", "\t \t", 1), s.replace("\t", " \t ", 2)])
238
+ add("tabs", s)
239
+ # control characters and invisible code points
240
+ for _ in range(120):
241
+ words = sentence(r).split(" ")
242
+ s = " ".join(w + (r.choice(CONTROLS) if r.random() < 0.35 else "") for w in words)
243
+ s = r.choice([s, r.choice(CONTROLS) + s, s + r.choice(CONTROLS), "".join(r.choice(CONTROLS) for _ in range(r.randint(1, 12)))])
244
+ add("control", s)
245
+ # special-token look-alikes, alone, embedded, adjacent, repeated
246
+ for _ in range(170):
247
+ toks = [r.choice(LOOKALIKES) for _ in range(r.randint(1, 5))]
248
+ words = sentence(r, ["en"]).split(" ")
249
+ pos = r.randint(0, len(words))
250
+ s = r.choice([" ".join(words[:pos] + toks + words[pos:]), "".join(toks), " ".join(toks),
251
+ "".join(words[:pos]) + "".join(toks) + "".join(words[pos:]), toks[0] * r.randint(2, 6)])
252
+ add("lookalike", s)
253
+ # very short / empty
254
+ for s in ["", " ", " ", "\n", "a", "é", "é", "▁", "▁a", "🙂", "<", ">", "|", "title:", "task:", "none", "x" * 1,
255
+ "\u0000", "퟿", "", "\U0010ffff", "0", "٠١٢٣", "१२३", "一", "あ", "한"]:
256
+ add("short", s, "raw")
257
+ add("short", s, "d")
258
+ # random scalar soup (assigned, unassigned, private use, non-characters)
259
+ for _ in range(220):
260
+ s = "".join(rand_scalar(r) for _ in range(r.randint(1, 60)))
261
+ add("soup", s)
262
+ # mixed everything
263
+ for _ in range(200):
264
+ bits = [sentence(r), r.choice(EMOJI), r.choice(LOOKALIKES), r.choice(CONTROLS), r.choice(WS) * r.randint(1, 40),
265
+ fill(r.choice(CODE), r), unicodedata.normalize("NFD", fill(r.choice(ACCENTED), r)), "".join(rand_scalar(r) for _ in range(5))]
266
+ r.shuffle(bits)
267
+ add("mixed", "".join(bits[: r.randint(2, 8)]))
268
+ # very long inputs (5k .. 80k characters)
269
+ for i in range(22):
270
+ n = [5000, 8000, 12000, 20000, 30000, 50000, 80000][i % 7]
271
+ parts = []
272
+ while sum(len(p) for p in parts) < n:
273
+ parts.append(r.choice([sentence(r), fill(r.choice(CODE), r), r.choice(EMOJI), r.choice(WS) * r.randint(1, 50),
274
+ "".join(rand_scalar(r) for _ in range(20)), r.choice(LOOKALIKES)]))
275
+ add("long", " ".join(parts), "d")
276
+ return out
277
+
278
+
279
+ def main() -> None:
280
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
281
+ ap.add_argument("--tokenizer", required=True, help="path to tokenizer.json")
282
+ ap.add_argument("--out", required=True)
283
+ ap.add_argument("--seed", type=int, default=20261012)
284
+ args = ap.parse_args()
285
+ from tokenizers import Tokenizer
286
+
287
+ tok = Tokenizer.from_file(args.tokenizer)
288
+ cases = build(random.Random(args.seed))
289
+ for c in cases:
290
+ c["ids"] = [2] + tok.encode(PREFIX[c["kind"]] + c["text"], add_special_tokens=False).ids + [1]
291
+ Path(args.out).write_text(json.dumps(cases, ensure_ascii=False), encoding="utf-8")
292
+ from collections import Counter
293
+ print(f"{len(cases)} cases, {sum(len(c['ids']) for c in cases)} ids ->", args.out)
294
+ print(dict(Counter(c["cat"] for c in cases)))
295
+
296
+
297
+ if __name__ == "__main__":
298
+ main()
tests/smoke-reference.json ADDED
@@ -0,0 +1,2333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "reference": "sentence-transformers fp32 on CPU, original checkpoint, text only",
3
+ "versions": {
4
+ "sentence_transformers": "6.1.0",
5
+ "transformers": "5.19.0",
6
+ "torch": "2.14.1"
7
+ },
8
+ "threshold_cosine": 0.999,
9
+ "cases": [
10
+ {
11
+ "kind": "search_query",
12
+ "text": "When does the warehouse lease in Brindlemoor expire?",
13
+ "embedding": [
14
+ 0.0140514,
15
+ 0.0200849,
16
+ 0.0201675,
17
+ -0.0119595,
18
+ -0.035274,
19
+ 0.0034859,
20
+ -0.0322359,
21
+ 0.0260318,
22
+ -0.0253003,
23
+ -0.1322919,
24
+ 0.0172573,
25
+ -0.0929851,
26
+ -0.0345029,
27
+ 0.0034794,
28
+ 0.0074408,
29
+ -0.0231558,
30
+ -0.0121178,
31
+ 0.0041102,
32
+ -0.026006,
33
+ -0.0039767,
34
+ -0.0500095,
35
+ -0.0474389,
36
+ 0.0046259,
37
+ -0.0144053,
38
+ 0.0104898,
39
+ -0.0104195,
40
+ -0.0212026,
41
+ -0.011046,
42
+ -0.0204922,
43
+ 0.1905717,
44
+ -0.0039958,
45
+ -0.0352325,
46
+ -0.0434769,
47
+ -0.0078017,
48
+ 0.0146712,
49
+ -0.0420867,
50
+ -0.0191855,
51
+ 0.0736523,
52
+ 0.0112718,
53
+ 0.0299183,
54
+ -0.0180284,
55
+ 0.0087481,
56
+ -0.0032612,
57
+ -0.0007796,
58
+ 0.0054152,
59
+ 0.0057414,
60
+ 0.0320906,
61
+ 0.0198112,
62
+ -0.0058027,
63
+ 0.0055605,
64
+ -0.011155,
65
+ 0.0018056,
66
+ -0.0036217,
67
+ 0.0599205,
68
+ -0.0395265,
69
+ 0.0071835,
70
+ 0.0045348,
71
+ 0.0249366,
72
+ 0.0060176,
73
+ -0.0363902,
74
+ -0.0180425,
75
+ -0.0019947,
76
+ 0.013761,
77
+ 0.0338091,
78
+ -0.0665499,
79
+ -0.0047037,
80
+ 0.0470355,
81
+ -0.0196299,
82
+ 0.0428396,
83
+ 0.0634703,
84
+ 0.0617316,
85
+ 0.0512214,
86
+ 0.0480056,
87
+ 0.0396131,
88
+ -0.0358877,
89
+ -0.0718305,
90
+ -0.102156,
91
+ 0.0629865,
92
+ -0.0244289,
93
+ -0.0405096,
94
+ 0.0616369,
95
+ 0.0004009,
96
+ -0.0722917,
97
+ -0.0258843,
98
+ -0.037575,
99
+ -0.0455716,
100
+ -0.0343866,
101
+ 0.0075406,
102
+ 0.035479,
103
+ 0.0328424,
104
+ -0.008169,
105
+ 0.0508599,
106
+ -0.0598774,
107
+ -0.0104931,
108
+ -0.067505,
109
+ 0.0006392,
110
+ -0.0198671,
111
+ 0.0011432,
112
+ 0.0123858,
113
+ -0.0174422,
114
+ 0.0056807,
115
+ -0.0079437,
116
+ -0.0044856,
117
+ 0.0179937,
118
+ 0.0756269,
119
+ 0.0363916,
120
+ -0.0265536,
121
+ 0.0695819,
122
+ 0.040324,
123
+ -0.0622708,
124
+ -0.064421,
125
+ -0.2657686,
126
+ -0.0183439,
127
+ 0.0499475,
128
+ 0.0405476,
129
+ 0.2633377,
130
+ -0.0433234,
131
+ -0.0144355,
132
+ -0.0109114,
133
+ 0.0286458,
134
+ 0.0041431,
135
+ 0.0063956,
136
+ -0.0419718,
137
+ 0.0039493,
138
+ -0.057251,
139
+ -0.0082195,
140
+ 0.0578189,
141
+ 0.0176498,
142
+ 0.0127398,
143
+ 0.0349538,
144
+ 0.0112667,
145
+ -0.0230163,
146
+ 0.0316641,
147
+ 0.0500062,
148
+ -0.0358909,
149
+ -0.0121331,
150
+ -0.0285246,
151
+ -0.0099984,
152
+ -0.0417251,
153
+ -0.015272,
154
+ -0.0617455,
155
+ -0.0135424,
156
+ 0.015151,
157
+ 0.0473345,
158
+ -0.0048397,
159
+ 0.0277582,
160
+ -0.0336748,
161
+ -0.032164,
162
+ 0.0029421,
163
+ 0.0061318,
164
+ 0.0053797,
165
+ -0.0440732,
166
+ -0.0239873,
167
+ 0.0194475,
168
+ 0.0319975,
169
+ 0.0183903,
170
+ 0.0482337,
171
+ -0.007429,
172
+ -0.0113159,
173
+ -0.0271933,
174
+ -0.0209385,
175
+ 0.002265,
176
+ 0.017666,
177
+ 0.0208267,
178
+ 0.0063856,
179
+ 0.0297219,
180
+ -0.0184708,
181
+ 0.025398,
182
+ 0.0026902,
183
+ 0.0512845,
184
+ -0.0034231,
185
+ -0.0198501,
186
+ -0.000227,
187
+ -0.0331328,
188
+ 0.0207864,
189
+ 0.0023767,
190
+ 0.0558418,
191
+ -0.0101271,
192
+ -0.0198393,
193
+ 0.0030498,
194
+ -0.0618708,
195
+ -0.0085252,
196
+ -0.0102823,
197
+ -0.0304731,
198
+ 0.0326227,
199
+ 0.0539232,
200
+ -0.0254712,
201
+ 0.003854,
202
+ -0.0184922,
203
+ -0.0071012,
204
+ 0.0024879,
205
+ 0.0039352,
206
+ 0.0036627,
207
+ 0.0092597,
208
+ -0.0218288,
209
+ 0.0062251,
210
+ 0.0374328,
211
+ -0.012431,
212
+ -0.0118201,
213
+ 0.0184596,
214
+ 0.0124813,
215
+ -0.0153233,
216
+ 0.0424144,
217
+ -0.0017183,
218
+ -0.0481435,
219
+ 0.0003325,
220
+ -0.0335241,
221
+ 0.0318867,
222
+ 0.0052809,
223
+ -0.0104564,
224
+ 0.0022734,
225
+ -0.0028741,
226
+ 0.0161565,
227
+ -0.0500812,
228
+ 0.0328645,
229
+ 0.0240628,
230
+ -0.0186112,
231
+ 0.0400808,
232
+ 0.0222267,
233
+ -0.025151,
234
+ -0.006919,
235
+ 0.0431995,
236
+ 0.0789402,
237
+ 0.0139404,
238
+ -0.0049106,
239
+ -0.0538642,
240
+ 0.0063854,
241
+ -0.0147278,
242
+ -0.0038739,
243
+ 0.056846,
244
+ -0.0180968,
245
+ 0.0210576,
246
+ -0.0370839,
247
+ -0.088229,
248
+ 0.0906495,
249
+ -0.0048958,
250
+ -0.0581601,
251
+ 0.0112045,
252
+ -0.0083936,
253
+ 0.0677319,
254
+ -0.0318846,
255
+ -0.0155604,
256
+ 0.0173322,
257
+ 0.061947,
258
+ -0.0243701,
259
+ -0.0212206,
260
+ -0.0025019,
261
+ 0.0102068,
262
+ 0.0122189,
263
+ -0.0119761,
264
+ 0.0699356,
265
+ -0.0088522,
266
+ -0.0546333,
267
+ -0.0722661,
268
+ -0.015771,
269
+ -0.0367368,
270
+ 0.0324103,
271
+ -0.0347873,
272
+ 0.0060024,
273
+ 0.0400301,
274
+ -0.0096935,
275
+ -0.0621471,
276
+ -0.0305385,
277
+ 0.024493,
278
+ 0.004069,
279
+ 0.0410262,
280
+ -0.0622035,
281
+ 0.0020669,
282
+ 0.0152834,
283
+ -0.0156231,
284
+ 0.0165897,
285
+ 0.0094615,
286
+ 0.0054113,
287
+ 0.0058183,
288
+ 0.0291583,
289
+ 0.0337521,
290
+ -0.0329887,
291
+ 0.044419,
292
+ 0.0099162,
293
+ -0.0130374,
294
+ 0.0149398,
295
+ -0.0300086,
296
+ -0.0443118,
297
+ 0.0060803,
298
+ -0.0163578,
299
+ 0.0493293,
300
+ -0.0172757,
301
+ 0.0178506,
302
+ 0.0246122,
303
+ 0.0469199,
304
+ 0.0338066,
305
+ -0.035952,
306
+ 0.0218767,
307
+ 0.0120018,
308
+ -0.028764,
309
+ 0.032157,
310
+ -0.0097965,
311
+ 0.0423428,
312
+ 0.0040818,
313
+ 0.0187548,
314
+ -0.0104548,
315
+ 0.0285643,
316
+ -0.0246368,
317
+ -0.032603,
318
+ 0.0659568,
319
+ -0.0442716,
320
+ -0.0681514,
321
+ -0.022396,
322
+ -0.0112234,
323
+ -0.0442733,
324
+ -0.0107434,
325
+ -0.0283538,
326
+ -0.0115352,
327
+ -0.02458,
328
+ 0.0270112,
329
+ 0.0093793,
330
+ -0.0185534,
331
+ 0.0137447,
332
+ 0.0031274,
333
+ -0.079896,
334
+ -0.0106351,
335
+ 0.0440398,
336
+ -0.0201902,
337
+ -0.0351441,
338
+ 0.0031468,
339
+ 0.0466262,
340
+ -0.0217505,
341
+ -0.0193922,
342
+ 0.0082847,
343
+ -0.033297,
344
+ -0.0407191,
345
+ 0.0030198,
346
+ -0.0266054,
347
+ 0.0127826,
348
+ 0.0090996,
349
+ 0.0228752,
350
+ 0.0433155,
351
+ 0.0605334,
352
+ 0.0233076,
353
+ -0.0209052,
354
+ -0.021734,
355
+ -0.1376118,
356
+ -0.062436,
357
+ 0.0284235,
358
+ 0.021142,
359
+ -0.0097345,
360
+ 0.003243,
361
+ 0.0095441,
362
+ -0.0628982,
363
+ 0.0300183,
364
+ -0.0005559,
365
+ 0.0380125,
366
+ 0.0286191,
367
+ -0.0209423,
368
+ -0.0044779,
369
+ 0.0035791,
370
+ 0.0474121,
371
+ 0.0384367,
372
+ 0.00689,
373
+ 0.0448739,
374
+ -0.0003337,
375
+ 0.0272092,
376
+ -0.0048142,
377
+ 0.0293282,
378
+ -0.0084391,
379
+ -0.0278422,
380
+ -0.0488862,
381
+ 0.0090513,
382
+ -0.0323157,
383
+ 0.0215345,
384
+ -0.0074275,
385
+ 0.013417,
386
+ -0.0262015,
387
+ 0.0041043,
388
+ 0.0173136,
389
+ 0.0766509,
390
+ 0.0014924,
391
+ 0.0104271,
392
+ -0.0027695,
393
+ -0.0264011,
394
+ 0.0729229,
395
+ 0.0227126,
396
+ -0.0357576,
397
+ 0.0224196,
398
+ 0.0429215,
399
+ -0.0572116,
400
+ 0.030736,
401
+ -0.0222465,
402
+ 0.0291479,
403
+ 0.0198262,
404
+ 0.0098066,
405
+ 0.2381135,
406
+ 0.0009308,
407
+ -0.0170637,
408
+ 0.0399403,
409
+ 0.0244772,
410
+ -0.0218695,
411
+ 0.013202,
412
+ -0.0195465,
413
+ -0.009753,
414
+ 0.0249087,
415
+ 0.0040551,
416
+ -0.0182575,
417
+ 0.0128712,
418
+ -0.0166567,
419
+ -0.0183231,
420
+ -0.0034079,
421
+ -0.0336708,
422
+ -0.0235771,
423
+ 0.0139053,
424
+ -0.0131362,
425
+ 0.0313214,
426
+ 0.0032543,
427
+ -0.0036488,
428
+ -0.0122029,
429
+ -0.0135203,
430
+ -0.0332577,
431
+ -0.0270707,
432
+ -0.0539252,
433
+ -0.0376571,
434
+ 0.0039163,
435
+ 0.0268505,
436
+ -0.009067,
437
+ -0.0237761,
438
+ 0.0446752,
439
+ 0.015811,
440
+ -0.0068362,
441
+ 0.0052214,
442
+ -0.004454,
443
+ 0.0399815,
444
+ 0.023899,
445
+ -0.031928,
446
+ -0.0199918,
447
+ 0.0048249,
448
+ 0.0395325,
449
+ 0.0104519,
450
+ 0.0263888,
451
+ -0.0090891,
452
+ -0.0206043,
453
+ -0.0156751,
454
+ 0.0082406,
455
+ -0.0057344,
456
+ -0.0070633,
457
+ 0.0328125,
458
+ 0.0125726,
459
+ -0.0467525,
460
+ -0.0043885,
461
+ -0.0173589,
462
+ -0.0055486,
463
+ 0.0196809,
464
+ -0.0101459,
465
+ -0.0079408,
466
+ 0.0167622,
467
+ 0.0093635,
468
+ -0.0186476,
469
+ -0.0031106,
470
+ -0.0500078,
471
+ -0.0372527,
472
+ 0.0440114,
473
+ 0.008821,
474
+ -0.0034126,
475
+ -0.0175557,
476
+ -0.005297,
477
+ -0.0138077,
478
+ 0.0122492,
479
+ 0.0161653,
480
+ -0.0163164,
481
+ -0.0130119,
482
+ -0.0301623,
483
+ -0.0263716,
484
+ -0.0042851,
485
+ 0.0190618,
486
+ -0.0150112,
487
+ 0.0062657,
488
+ -0.0044136,
489
+ -0.0191775,
490
+ 0.0154332,
491
+ 0.0233396,
492
+ 0.0147409,
493
+ -0.0016429,
494
+ 0.0397183,
495
+ -0.0267775,
496
+ -0.0249655,
497
+ -0.0084888,
498
+ -0.0365686,
499
+ 0.0376978,
500
+ -0.0176997,
501
+ 0.0466568,
502
+ -0.0013101,
503
+ 0.0168162,
504
+ 0.0361352,
505
+ 0.0076559,
506
+ -0.038352,
507
+ 0.0192798,
508
+ -0.0334455,
509
+ -0.0148572,
510
+ 0.0290095,
511
+ -0.0101722,
512
+ -0.00072,
513
+ 0.0198838,
514
+ -0.0442767,
515
+ 0.0014474,
516
+ -0.024855,
517
+ 0.0167283,
518
+ -0.0336968,
519
+ -0.0121257,
520
+ -0.0358068,
521
+ -0.1305676,
522
+ 0.0021763,
523
+ 0.0454077,
524
+ 0.0026204,
525
+ 0.040022,
526
+ 0.0299537,
527
+ 0.0282371,
528
+ 0.0095981,
529
+ -0.0063128,
530
+ 0.0010373,
531
+ 0.0163293,
532
+ -0.0015985,
533
+ 0.0251838,
534
+ 0.0284173,
535
+ -0.028472,
536
+ -0.009489,
537
+ -0.0275945,
538
+ -0.0364336,
539
+ -0.0283635,
540
+ 0.0044131,
541
+ 0.0035401,
542
+ 0.0103233,
543
+ -0.0083207,
544
+ 0.0274399,
545
+ 0.0147917,
546
+ -0.0156048,
547
+ -0.0215956,
548
+ -0.0052218,
549
+ -0.0390134,
550
+ -0.0142176,
551
+ -0.0004916,
552
+ 0.0027648,
553
+ -0.0229333,
554
+ 0.0540044,
555
+ -0.0389055,
556
+ -0.003413,
557
+ 0.0124384,
558
+ -0.0203941,
559
+ 0.0389145,
560
+ 0.0146834,
561
+ 0.0001552,
562
+ -0.0018129,
563
+ 0.0674664,
564
+ 0.0412302,
565
+ 0.0415348,
566
+ -0.0023984,
567
+ -0.0284114,
568
+ -0.2059626,
569
+ -0.0090965,
570
+ -0.0156103,
571
+ 0.0002269,
572
+ -0.0365953,
573
+ -0.0244425,
574
+ 0.0057417,
575
+ 0.048451,
576
+ 0.0383219,
577
+ -0.0023854,
578
+ -0.0357098,
579
+ 0.0177072,
580
+ -0.021637,
581
+ 0.0201444,
582
+ -0.0428248,
583
+ 0.0057723,
584
+ -0.0342198,
585
+ 0.0354257,
586
+ -0.0277822,
587
+ 0.0002803,
588
+ 0.0401719,
589
+ -0.0344315,
590
+ 0.0256965,
591
+ 0.0327575,
592
+ 0.0532876,
593
+ 0.0113293,
594
+ -0.0113227,
595
+ 0.0346123,
596
+ -0.0359698,
597
+ -0.0094106,
598
+ 0.0203509,
599
+ -0.0320784,
600
+ -0.0114563,
601
+ -0.0019832,
602
+ -0.0448561,
603
+ 0.0305796,
604
+ 0.0081664,
605
+ -0.0176064,
606
+ 0.0073882,
607
+ -0.015993,
608
+ -0.0134097,
609
+ -0.031895,
610
+ 0.0357883,
611
+ -0.0018839,
612
+ -0.0136349,
613
+ 0.0152895,
614
+ -0.0087174,
615
+ -0.013247,
616
+ 0.0096113,
617
+ 0.0168226,
618
+ -0.0005419,
619
+ -0.0159698,
620
+ 0.0096558,
621
+ -0.0327691,
622
+ -0.0433982,
623
+ -0.0066851,
624
+ 0.0044284,
625
+ 0.0213365,
626
+ 0.0320568,
627
+ 0.0022381,
628
+ 0.0257285,
629
+ 0.0143513,
630
+ 0.0022292,
631
+ -0.0166571,
632
+ -0.0146441,
633
+ -0.0404922,
634
+ -0.0189938,
635
+ -0.0101463,
636
+ 0.0187055,
637
+ 0.0037433,
638
+ 0.0022404,
639
+ -0.0062486,
640
+ -0.0070731,
641
+ 0.0032853,
642
+ 0.0411538,
643
+ -0.0119422,
644
+ -0.0371468,
645
+ 0.0130075,
646
+ 0.0291758,
647
+ -0.0188306,
648
+ -0.0089265,
649
+ 0.0263609,
650
+ 0.0156603,
651
+ -0.004102,
652
+ -0.0210846,
653
+ 0.0158581,
654
+ -5.31e-05,
655
+ 0.0059412,
656
+ 0.0064431,
657
+ -0.0194571,
658
+ 0.0351863,
659
+ 0.0506362,
660
+ 0.0143477,
661
+ 0.0433141,
662
+ -0.0236757,
663
+ 0.0323491,
664
+ 0.018349,
665
+ 0.0509639,
666
+ 0.0147666,
667
+ -0.0070182,
668
+ -0.002872,
669
+ -0.0116239,
670
+ -0.0142457,
671
+ 0.0087374,
672
+ -0.0020458,
673
+ 0.0024226,
674
+ 0.0514395,
675
+ -0.0161209,
676
+ 0.0376469,
677
+ 0.013314,
678
+ -0.0078508,
679
+ 0.0112804,
680
+ -0.0185661,
681
+ 0.0004649,
682
+ -0.0206176,
683
+ 0.0290994,
684
+ -0.0127025,
685
+ -0.0008931,
686
+ -0.0125401,
687
+ -0.011069,
688
+ -0.01393,
689
+ -0.0161357,
690
+ -0.0261728,
691
+ -0.0188071,
692
+ 0.0273512,
693
+ 0.0359035,
694
+ -0.0228979,
695
+ -0.0147417,
696
+ 0.0048217,
697
+ 0.0288668,
698
+ 0.0073423,
699
+ 0.0100393,
700
+ 0.041307,
701
+ 0.0198599,
702
+ 0.0067079,
703
+ -0.0030872,
704
+ -0.0128413,
705
+ 0.0158811,
706
+ 0.0128299,
707
+ 0.0302011,
708
+ 0.009724,
709
+ -0.0548747,
710
+ 0.0092055,
711
+ -0.0272114,
712
+ -0.024287,
713
+ 0.0015032,
714
+ 0.0542283,
715
+ 0.0184035,
716
+ 0.02366,
717
+ 0.0377193,
718
+ 0.0221289,
719
+ 0.0158905,
720
+ 0.0122361,
721
+ -0.0181141,
722
+ -0.0064896,
723
+ 0.0225127,
724
+ 0.0133703,
725
+ -0.0149417,
726
+ -0.028168,
727
+ 0.0244618,
728
+ -0.0035144,
729
+ -0.0209218,
730
+ 0.0446331,
731
+ 0.0127702,
732
+ -0.0186521,
733
+ 0.0005753,
734
+ -0.0012075,
735
+ -0.0183204,
736
+ 0.0400032,
737
+ 0.0197334,
738
+ 0.0030626,
739
+ -0.0455221,
740
+ 0.0269828,
741
+ -0.0142443,
742
+ -0.0115392,
743
+ 0.0330398,
744
+ 0.0007724,
745
+ 0.0340609,
746
+ 0.2108322,
747
+ 0.0039355,
748
+ -0.0055149,
749
+ 0.0161179,
750
+ -0.0071948,
751
+ -0.0103065,
752
+ 0.0139283,
753
+ 0.0374079,
754
+ -0.0069936,
755
+ 0.0435351,
756
+ 0.0173975,
757
+ 0.0105077,
758
+ 0.0359319,
759
+ -0.0259694,
760
+ -0.0158281,
761
+ -0.0211381,
762
+ 0.029089,
763
+ -0.0099361,
764
+ 0.022188,
765
+ 0.0078775,
766
+ 0.0127904,
767
+ -0.0391305,
768
+ -0.0249713,
769
+ 0.0108958,
770
+ -0.0368597,
771
+ 0.0160098,
772
+ -0.0362015,
773
+ 0.0072925,
774
+ -0.0076356,
775
+ 0.0512759,
776
+ -0.0074541,
777
+ -0.0033625,
778
+ -0.0056429,
779
+ 0.0345776,
780
+ -0.016772,
781
+ 0.016576
782
+ ]
783
+ },
784
+ {
785
+ "kind": "document",
786
+ "text": "The warehouse lease in Brindlemoor runs until the last day of March; the landlord agreed to fix the window before then.",
787
+ "embedding": [
788
+ 0.0264644,
789
+ -0.0242984,
790
+ 0.0116434,
791
+ -0.0094456,
792
+ -0.0538755,
793
+ -0.0034127,
794
+ 0.01068,
795
+ 0.0363724,
796
+ -0.0184347,
797
+ -0.1608366,
798
+ 0.0270172,
799
+ -0.0312475,
800
+ 0.0268842,
801
+ 0.0346971,
802
+ -0.0035566,
803
+ -0.0575296,
804
+ -0.0185978,
805
+ -0.0473271,
806
+ -0.0191439,
807
+ -0.006971,
808
+ -0.0709102,
809
+ -0.0006216,
810
+ -0.0111378,
811
+ -0.0304192,
812
+ 0.0180717,
813
+ -0.0026377,
814
+ -0.0228687,
815
+ -0.0221057,
816
+ -0.0491751,
817
+ 0.173117,
818
+ -0.0081147,
819
+ -0.0180295,
820
+ -0.0083443,
821
+ -0.0130251,
822
+ 0.0615742,
823
+ -0.0135436,
824
+ -0.0290864,
825
+ 0.0527058,
826
+ -0.0001754,
827
+ 0.0241153,
828
+ -0.0342266,
829
+ 0.0070813,
830
+ -0.0127815,
831
+ 0.004976,
832
+ 0.0269828,
833
+ 0.0039893,
834
+ 0.0317817,
835
+ 0.0179321,
836
+ -0.0148058,
837
+ 0.0165759,
838
+ 0.0083154,
839
+ 0.0053715,
840
+ -0.000684,
841
+ 0.0518343,
842
+ -0.0072164,
843
+ 0.0328747,
844
+ 0.0133487,
845
+ 0.0612634,
846
+ -0.0099483,
847
+ -0.0133693,
848
+ 0.0050172,
849
+ -0.0203206,
850
+ 0.0005791,
851
+ 0.0136871,
852
+ -0.0553643,
853
+ 0.0312551,
854
+ 0.0846288,
855
+ -0.0247101,
856
+ 0.0219906,
857
+ 0.0775483,
858
+ 0.0239536,
859
+ -6.89e-05,
860
+ 0.0347904,
861
+ 0.0412152,
862
+ -0.0377688,
863
+ -0.0377716,
864
+ -0.107596,
865
+ 0.0521024,
866
+ -0.0165523,
867
+ -0.0078278,
868
+ 0.0289683,
869
+ 0.0188598,
870
+ -0.0292757,
871
+ 0.0088558,
872
+ 0.0036091,
873
+ -0.052127,
874
+ 0.0031963,
875
+ 0.012254,
876
+ -0.0349788,
877
+ 0.0253031,
878
+ 0.0281118,
879
+ 0.0375488,
880
+ -0.0521213,
881
+ 0.0173015,
882
+ -0.0504028,
883
+ 0.0084912,
884
+ 0.0133818,
885
+ 0.0270663,
886
+ -0.0064787,
887
+ 0.0219093,
888
+ 0.0043792,
889
+ 0.0296521,
890
+ -0.0094517,
891
+ 0.0395419,
892
+ 0.0519162,
893
+ 0.0129672,
894
+ -0.0317267,
895
+ 0.0651949,
896
+ 0.0392844,
897
+ -0.0268617,
898
+ -0.0067156,
899
+ -0.2840129,
900
+ -0.0304729,
901
+ 0.0118219,
902
+ 0.044337,
903
+ 0.3071545,
904
+ -0.0170552,
905
+ -0.0233961,
906
+ 0.0009735,
907
+ 0.0657148,
908
+ -0.0084815,
909
+ 0.0454244,
910
+ -0.0533229,
911
+ 0.0050689,
912
+ -0.0576392,
913
+ -0.0074852,
914
+ 0.0658127,
915
+ -0.0100089,
916
+ -0.008696,
917
+ 0.0184354,
918
+ -0.0248318,
919
+ -0.0052167,
920
+ 0.0300652,
921
+ 0.0693946,
922
+ -0.0166476,
923
+ -0.0157638,
924
+ -0.031577,
925
+ -0.0206295,
926
+ -0.0082261,
927
+ -0.0169719,
928
+ -0.0148738,
929
+ 0.0184738,
930
+ 0.0035616,
931
+ 0.0650747,
932
+ -0.0467764,
933
+ 0.0229762,
934
+ -0.019042,
935
+ 0.0011456,
936
+ 0.0141589,
937
+ -0.0006831,
938
+ 0.0390894,
939
+ -0.0008999,
940
+ -0.0218775,
941
+ -0.0199317,
942
+ 0.0193458,
943
+ 0.0177277,
944
+ 0.0344862,
945
+ 0.025085,
946
+ -0.0311837,
947
+ -0.0123519,
948
+ -0.030943,
949
+ 0.0107378,
950
+ 0.0289433,
951
+ 0.008099,
952
+ -0.0235138,
953
+ 0.0252559,
954
+ -0.06868,
955
+ 0.017455,
956
+ 0.0419686,
957
+ 0.0408476,
958
+ 0.0254791,
959
+ -0.0242629,
960
+ -0.0216531,
961
+ -0.0102706,
962
+ 0.0281387,
963
+ -0.0112468,
964
+ 0.0444572,
965
+ -0.0275173,
966
+ 0.0002826,
967
+ 0.0086426,
968
+ -0.0212904,
969
+ -0.0579055,
970
+ 0.0215879,
971
+ -0.0140405,
972
+ 0.0252201,
973
+ 0.0211788,
974
+ -0.0142696,
975
+ -0.0234745,
976
+ -0.0227191,
977
+ 0.0323166,
978
+ 0.0026156,
979
+ 0.0010938,
980
+ 0.0220431,
981
+ -0.0217016,
982
+ -0.0015277,
983
+ -0.0262872,
984
+ 0.0427692,
985
+ 0.0093728,
986
+ 0.0139956,
987
+ 0.0309493,
988
+ 0.0226167,
989
+ -0.0272248,
990
+ 0.0383349,
991
+ 0.0175825,
992
+ -0.0447357,
993
+ -0.001296,
994
+ -0.0477331,
995
+ -0.0072662,
996
+ -0.0054068,
997
+ -0.019633,
998
+ 0.0323781,
999
+ -0.0285428,
1000
+ 0.0375163,
1001
+ 0.000403,
1002
+ -0.0015404,
1003
+ 0.0526022,
1004
+ 0.0112267,
1005
+ 0.0903424,
1006
+ -0.0030999,
1007
+ -0.0105379,
1008
+ 0.0019585,
1009
+ 0.0412905,
1010
+ 0.0871157,
1011
+ 0.0094184,
1012
+ -0.023252,
1013
+ -0.0291806,
1014
+ 0.0024612,
1015
+ -0.0049254,
1016
+ -0.0248866,
1017
+ 0.0566803,
1018
+ 0.0235696,
1019
+ 0.0388389,
1020
+ -0.0158421,
1021
+ -0.0421797,
1022
+ 0.0584066,
1023
+ 0.0114919,
1024
+ -0.0320118,
1025
+ -0.010658,
1026
+ -0.0164602,
1027
+ 0.0407591,
1028
+ -0.0285565,
1029
+ 0.0040359,
1030
+ -0.0325882,
1031
+ 0.0494892,
1032
+ -0.0049442,
1033
+ -0.0034794,
1034
+ 0.0059856,
1035
+ -0.0019577,
1036
+ 0.0305299,
1037
+ -0.0129538,
1038
+ 0.0508744,
1039
+ 0.0091949,
1040
+ -0.0272065,
1041
+ -0.0722709,
1042
+ 0.0146036,
1043
+ -0.0279869,
1044
+ 0.042139,
1045
+ -0.0165828,
1046
+ -0.0148761,
1047
+ 0.0239786,
1048
+ -0.0248741,
1049
+ -0.037085,
1050
+ -0.0145703,
1051
+ 0.0046245,
1052
+ -0.0166312,
1053
+ 0.0376871,
1054
+ -0.0232947,
1055
+ -0.0080422,
1056
+ -0.0138715,
1057
+ 0.0056189,
1058
+ 0.0084751,
1059
+ -0.0106014,
1060
+ -0.0011364,
1061
+ -0.004992,
1062
+ 0.0041779,
1063
+ 0.0412414,
1064
+ -0.0332662,
1065
+ 0.0296032,
1066
+ -0.0010485,
1067
+ -0.0027399,
1068
+ 0.0034219,
1069
+ -0.0054038,
1070
+ -0.0211052,
1071
+ 0.0092755,
1072
+ -0.0005159,
1073
+ 0.0146269,
1074
+ -0.0139352,
1075
+ 0.0105331,
1076
+ 0.0190807,
1077
+ 0.0334958,
1078
+ 0.0569536,
1079
+ -0.0163442,
1080
+ 0.0040735,
1081
+ 0.0267959,
1082
+ -0.020799,
1083
+ 0.0280741,
1084
+ -0.0202714,
1085
+ 0.037818,
1086
+ 0.0111139,
1087
+ 0.0095929,
1088
+ -0.0142725,
1089
+ -0.0020434,
1090
+ -0.0200119,
1091
+ 0.0226144,
1092
+ 0.0670394,
1093
+ -0.010802,
1094
+ -0.0725678,
1095
+ -0.0035054,
1096
+ -0.0368582,
1097
+ -0.0428894,
1098
+ -0.0163187,
1099
+ -0.0220368,
1100
+ -0.02653,
1101
+ -0.0019634,
1102
+ 0.0407205,
1103
+ 0.0271887,
1104
+ -0.0069772,
1105
+ -0.0075532,
1106
+ -0.0117692,
1107
+ -0.0747322,
1108
+ -0.0010772,
1109
+ 0.0234839,
1110
+ -0.0173247,
1111
+ -0.025903,
1112
+ 0.00921,
1113
+ 0.0320551,
1114
+ -0.0262158,
1115
+ 0.0017413,
1116
+ -0.0307316,
1117
+ -0.0400242,
1118
+ -0.0027447,
1119
+ -0.0017768,
1120
+ -0.0218839,
1121
+ 0.0246199,
1122
+ 0.0165034,
1123
+ -0.0065996,
1124
+ 0.0269161,
1125
+ 0.0112707,
1126
+ 0.0273772,
1127
+ -0.0098787,
1128
+ -0.0179007,
1129
+ -0.1398847,
1130
+ -0.010778,
1131
+ 3.89e-05,
1132
+ 0.0183281,
1133
+ 0.0210552,
1134
+ 0.0155068,
1135
+ 0.0270709,
1136
+ -0.0392508,
1137
+ 0.0251768,
1138
+ 0.0052068,
1139
+ 0.0513872,
1140
+ 0.0119341,
1141
+ -0.0019493,
1142
+ -0.0304656,
1143
+ 0.0209176,
1144
+ 0.0601201,
1145
+ 0.0198429,
1146
+ 0.0126918,
1147
+ 0.0168446,
1148
+ -0.0221235,
1149
+ 0.0020831,
1150
+ 0.0003669,
1151
+ 0.0489466,
1152
+ -0.0236555,
1153
+ -0.0086362,
1154
+ -0.0242799,
1155
+ 0.016296,
1156
+ -0.0046918,
1157
+ 0.0240075,
1158
+ 0.0036074,
1159
+ 0.0125185,
1160
+ 0.0037332,
1161
+ 0.0026463,
1162
+ 0.0287793,
1163
+ 0.0442534,
1164
+ -0.0008671,
1165
+ -0.0068142,
1166
+ -0.0095245,
1167
+ 0.0267455,
1168
+ 0.0584806,
1169
+ 0.0285134,
1170
+ 0.0102658,
1171
+ 0.0434944,
1172
+ 0.0367759,
1173
+ -0.0005722,
1174
+ 0.0451684,
1175
+ 0.0049044,
1176
+ 0.0289554,
1177
+ 0.0256482,
1178
+ -0.0042619,
1179
+ 0.286271,
1180
+ 0.0199974,
1181
+ 0.0035183,
1182
+ 0.0286582,
1183
+ 0.0405934,
1184
+ -0.0167135,
1185
+ 0.0291236,
1186
+ -0.0018019,
1187
+ -0.0219017,
1188
+ 0.003888,
1189
+ 0.0021741,
1190
+ 0.0179549,
1191
+ -0.0181287,
1192
+ -0.003402,
1193
+ 0.0241312,
1194
+ -0.0023668,
1195
+ -0.0459792,
1196
+ 0.0055813,
1197
+ -0.005714,
1198
+ -0.0212274,
1199
+ 0.0166356,
1200
+ 0.0252439,
1201
+ 0.0101637,
1202
+ 0.0045283,
1203
+ -0.0259132,
1204
+ -0.0325882,
1205
+ -0.0176896,
1206
+ -0.0400428,
1207
+ 0.0077737,
1208
+ -0.0154222,
1209
+ 0.0193038,
1210
+ -0.0162069,
1211
+ 0.0035849,
1212
+ 0.0416583,
1213
+ 0.009458,
1214
+ -0.0236569,
1215
+ 0.0048055,
1216
+ -0.0021436,
1217
+ 0.0053086,
1218
+ 0.0072111,
1219
+ 0.0059566,
1220
+ -0.0258894,
1221
+ 0.0079106,
1222
+ 0.0285769,
1223
+ 0.0098245,
1224
+ 0.036957,
1225
+ 0.0063089,
1226
+ -0.0089957,
1227
+ 0.0008741,
1228
+ 0.0068467,
1229
+ -0.0078629,
1230
+ -0.0002527,
1231
+ -0.0080054,
1232
+ -0.0158927,
1233
+ -0.0252407,
1234
+ -0.0111276,
1235
+ -0.028465,
1236
+ -0.0106257,
1237
+ 0.0402076,
1238
+ 0.0120167,
1239
+ 0.0064657,
1240
+ 0.0467545,
1241
+ -0.0183397,
1242
+ -0.000893,
1243
+ -0.0129492,
1244
+ -0.0484937,
1245
+ -0.0369712,
1246
+ 0.0168339,
1247
+ 0.0526176,
1248
+ -0.0011955,
1249
+ -0.0661879,
1250
+ -0.0166612,
1251
+ -0.0033391,
1252
+ 0.0409913,
1253
+ -0.0211322,
1254
+ 0.003656,
1255
+ -0.0014469,
1256
+ -0.0320232,
1257
+ -0.0187179,
1258
+ -0.0282663,
1259
+ 0.0106685,
1260
+ 0.0107463,
1261
+ -0.0215654,
1262
+ -0.0187622,
1263
+ -0.0152856,
1264
+ 0.0209789,
1265
+ 0.0112883,
1266
+ 0.0126507,
1267
+ -0.0026165,
1268
+ 0.0558417,
1269
+ -0.0469727,
1270
+ -0.0207666,
1271
+ -0.0208326,
1272
+ -0.0426097,
1273
+ -0.00665,
1274
+ 0.0050596,
1275
+ 0.0485706,
1276
+ -0.0045574,
1277
+ 0.023139,
1278
+ 0.0228496,
1279
+ -0.001325,
1280
+ -0.0225157,
1281
+ 0.0214948,
1282
+ -0.0123868,
1283
+ -0.0100371,
1284
+ 0.0241164,
1285
+ -0.0104662,
1286
+ -0.0054405,
1287
+ 0.0180619,
1288
+ -0.0388385,
1289
+ 0.005823,
1290
+ 0.0250867,
1291
+ 0.0443838,
1292
+ 0.0081462,
1293
+ -0.0252075,
1294
+ 0.0075606,
1295
+ -0.134257,
1296
+ 0.0238103,
1297
+ 0.0403092,
1298
+ -0.033447,
1299
+ 0.0609221,
1300
+ 0.0102637,
1301
+ 0.031284,
1302
+ -0.0042293,
1303
+ 0.0090502,
1304
+ 0.0206708,
1305
+ 0.0153386,
1306
+ -0.0074572,
1307
+ 0.0034939,
1308
+ -0.0141006,
1309
+ -0.0052234,
1310
+ 0.0150197,
1311
+ -0.016243,
1312
+ -0.0435209,
1313
+ -0.0013051,
1314
+ 0.0232986,
1315
+ -0.0497696,
1316
+ -0.0017224,
1317
+ -0.0076917,
1318
+ 0.0083871,
1319
+ -0.0113315,
1320
+ -0.0265682,
1321
+ -0.0089959,
1322
+ -0.0379045,
1323
+ -0.0246575,
1324
+ -0.0098334,
1325
+ -0.004501,
1326
+ 0.0203684,
1327
+ -0.0072931,
1328
+ 0.0307912,
1329
+ 0.0030027,
1330
+ -0.0012177,
1331
+ 0.0056174,
1332
+ -0.0066306,
1333
+ 0.017854,
1334
+ -0.0046366,
1335
+ -0.0017239,
1336
+ 0.0141466,
1337
+ 0.0568046,
1338
+ 0.0336952,
1339
+ 0.0252416,
1340
+ 0.0015724,
1341
+ -0.0107673,
1342
+ -0.2303559,
1343
+ -0.0640194,
1344
+ 0.002422,
1345
+ 0.0279597,
1346
+ -4.16e-05,
1347
+ 0.0091563,
1348
+ -0.0084589,
1349
+ 0.0591685,
1350
+ 0.006867,
1351
+ 0.0021346,
1352
+ -0.0314672,
1353
+ 0.0068378,
1354
+ -0.0169414,
1355
+ 0.011671,
1356
+ -0.0496234,
1357
+ 0.0066084,
1358
+ -0.022313,
1359
+ 0.025086,
1360
+ -0.0206278,
1361
+ -0.0088476,
1362
+ 0.0645719,
1363
+ -0.0277695,
1364
+ 0.033116,
1365
+ 0.0418436,
1366
+ 0.0363094,
1367
+ 0.0308525,
1368
+ -0.0173255,
1369
+ -0.0032406,
1370
+ 0.0002146,
1371
+ -0.0165661,
1372
+ -0.0121446,
1373
+ -0.031358,
1374
+ -0.0283029,
1375
+ 0.0191103,
1376
+ -0.0352197,
1377
+ 0.0368655,
1378
+ 0.025689,
1379
+ -0.0201068,
1380
+ 0.0135953,
1381
+ -0.0169755,
1382
+ -0.0089089,
1383
+ -0.0187155,
1384
+ 0.0421347,
1385
+ 0.0302731,
1386
+ 0.0062562,
1387
+ -0.009368,
1388
+ -0.0101766,
1389
+ -0.0238247,
1390
+ 0.0058758,
1391
+ -0.0011821,
1392
+ 0.0071059,
1393
+ -0.0118324,
1394
+ 0.0148747,
1395
+ -0.0112144,
1396
+ -0.0351373,
1397
+ -0.0065663,
1398
+ -0.0335906,
1399
+ 0.013,
1400
+ 0.0116598,
1401
+ 0.0019158,
1402
+ -0.0010235,
1403
+ 0.0066092,
1404
+ -0.0084019,
1405
+ 0.0062142,
1406
+ -0.0070109,
1407
+ -0.0236729,
1408
+ -0.0142321,
1409
+ -1.57e-05,
1410
+ 0.0091445,
1411
+ 0.0293063,
1412
+ 0.0035479,
1413
+ 0.0109397,
1414
+ -0.0091237,
1415
+ 0.0246063,
1416
+ 0.0384086,
1417
+ -0.0161373,
1418
+ -0.0127944,
1419
+ 0.0076104,
1420
+ 0.0206074,
1421
+ 0.0328081,
1422
+ 0.0080331,
1423
+ 0.0056978,
1424
+ -0.0059247,
1425
+ 0.008972,
1426
+ -0.014289,
1427
+ 0.0126287,
1428
+ -0.0137993,
1429
+ 0.0074459,
1430
+ 0.0032179,
1431
+ -0.0166674,
1432
+ 0.0199677,
1433
+ 0.025108,
1434
+ -0.0031548,
1435
+ 0.0136954,
1436
+ -0.0395122,
1437
+ 0.041827,
1438
+ 0.002465,
1439
+ 0.050934,
1440
+ 0.0119671,
1441
+ -0.0135284,
1442
+ 0.0051843,
1443
+ -0.0149521,
1444
+ 0.0001085,
1445
+ -0.0003902,
1446
+ -0.0051798,
1447
+ 0.0069398,
1448
+ 0.0121671,
1449
+ -0.0026059,
1450
+ 0.0065326,
1451
+ -0.0162301,
1452
+ -0.0196698,
1453
+ 0.0183328,
1454
+ -0.0255839,
1455
+ 0.0067643,
1456
+ -0.023012,
1457
+ 0.0014238,
1458
+ -0.0116004,
1459
+ -0.0156751,
1460
+ -0.0077851,
1461
+ -0.0268469,
1462
+ -0.0117132,
1463
+ -0.0267632,
1464
+ -0.012754,
1465
+ -0.0475532,
1466
+ 0.009542,
1467
+ 0.0140083,
1468
+ -0.0277571,
1469
+ -0.0022977,
1470
+ 0.0071351,
1471
+ 0.0189572,
1472
+ -0.0126652,
1473
+ 0.0310627,
1474
+ 0.0322423,
1475
+ -0.0091997,
1476
+ 0.0089104,
1477
+ -0.0056425,
1478
+ -0.024342,
1479
+ 0.0111729,
1480
+ 0.0005422,
1481
+ 0.0169579,
1482
+ 0.0189936,
1483
+ -0.0415399,
1484
+ 0.021819,
1485
+ -0.0405018,
1486
+ 0.0107911,
1487
+ -0.0077887,
1488
+ 0.048318,
1489
+ 0.0013121,
1490
+ 0.017062,
1491
+ 0.0367175,
1492
+ 0.0018443,
1493
+ -0.0108327,
1494
+ 0.0287105,
1495
+ -0.024023,
1496
+ -0.0027745,
1497
+ 0.0187717,
1498
+ 0.0018345,
1499
+ -0.0108974,
1500
+ -0.0224133,
1501
+ 0.0256002,
1502
+ -0.0080634,
1503
+ -0.0265149,
1504
+ 0.0517693,
1505
+ 0.0269079,
1506
+ 0.0251603,
1507
+ 0.0053892,
1508
+ -0.0136671,
1509
+ 0.0394292,
1510
+ 0.0463492,
1511
+ 0.0071633,
1512
+ -0.0029102,
1513
+ -0.0272801,
1514
+ 0.0164657,
1515
+ -0.0011746,
1516
+ 0.0025788,
1517
+ 0.0265455,
1518
+ -0.0069759,
1519
+ 0.0037236,
1520
+ 0.2317612,
1521
+ 0.0251917,
1522
+ -0.0107766,
1523
+ -0.017,
1524
+ -0.0060105,
1525
+ -0.0122113,
1526
+ 0.0047453,
1527
+ 0.0309908,
1528
+ -0.0087099,
1529
+ 0.0553553,
1530
+ 0.012144,
1531
+ 0.0458252,
1532
+ 0.046074,
1533
+ -0.0357232,
1534
+ -0.0233153,
1535
+ 0.0017353,
1536
+ -0.0060374,
1537
+ -0.0035919,
1538
+ 0.020817,
1539
+ 0.0342276,
1540
+ 0.0057531,
1541
+ -0.0323938,
1542
+ -0.0293829,
1543
+ 0.0287774,
1544
+ -0.0663916,
1545
+ 0.0148558,
1546
+ -0.0343938,
1547
+ 0.0079967,
1548
+ -0.0117679,
1549
+ 0.0250412,
1550
+ 0.0329488,
1551
+ -0.0064652,
1552
+ -0.0493818,
1553
+ 0.0227841,
1554
+ -0.0245425,
1555
+ 0.0195178
1556
+ ]
1557
+ },
1558
+ {
1559
+ "kind": "document",
1560
+ "text": "Lista della spesa: latte d'avena, due limoni, riso basmati, coriandolo e croccantini per il cane.",
1561
+ "embedding": [
1562
+ 0.0170374,
1563
+ -0.044635,
1564
+ -0.0047019,
1565
+ -0.0229727,
1566
+ 0.0358784,
1567
+ 0.0070688,
1568
+ 0.0636706,
1569
+ -0.0095134,
1570
+ 0.0034211,
1571
+ -0.0978533,
1572
+ 0.02883,
1573
+ 0.0047634,
1574
+ -0.045708,
1575
+ -0.0344993,
1576
+ 0.0595915,
1577
+ -0.0239481,
1578
+ 0.0599293,
1579
+ -0.0019344,
1580
+ 0.0037584,
1581
+ 0.0237904,
1582
+ -0.0470828,
1583
+ 0.0045741,
1584
+ -0.0460006,
1585
+ 0.0576605,
1586
+ 0.0114495,
1587
+ -0.012251,
1588
+ 0.0123977,
1589
+ -0.0349354,
1590
+ -0.018569,
1591
+ 0.1733741,
1592
+ -0.0089963,
1593
+ -0.023553,
1594
+ -0.0083854,
1595
+ -0.0460702,
1596
+ 0.0420627,
1597
+ -0.0377996,
1598
+ -0.0119087,
1599
+ -0.0360215,
1600
+ -0.0097449,
1601
+ -0.0203342,
1602
+ -0.0063737,
1603
+ -0.0413225,
1604
+ 0.0101058,
1605
+ 0.0178437,
1606
+ 0.0132315,
1607
+ 0.0265682,
1608
+ 0.0069234,
1609
+ 0.026995,
1610
+ 0.003121,
1611
+ 0.0125665,
1612
+ 0.1006458,
1613
+ -0.0385414,
1614
+ 0.0088992,
1615
+ 0.1112718,
1616
+ 0.0027232,
1617
+ 0.0248341,
1618
+ 0.0098803,
1619
+ 0.0200804,
1620
+ 0.0097934,
1621
+ 0.0446817,
1622
+ 0.0187769,
1623
+ 0.0145933,
1624
+ -0.0395547,
1625
+ -0.021266,
1626
+ -0.0572196,
1627
+ 0.0402977,
1628
+ 0.0587767,
1629
+ 0.0088406,
1630
+ -0.0020986,
1631
+ -0.042525,
1632
+ -0.0556863,
1633
+ 0.0168584,
1634
+ 0.0026634,
1635
+ 0.0761056,
1636
+ -0.0228929,
1637
+ -0.0022126,
1638
+ -0.1013387,
1639
+ -0.0081393,
1640
+ -0.0288168,
1641
+ 0.0312328,
1642
+ -0.0225599,
1643
+ -0.0985526,
1644
+ 0.0337848,
1645
+ 0.0062871,
1646
+ 0.0147591,
1647
+ -0.0307018,
1648
+ -0.0281429,
1649
+ -0.0074711,
1650
+ -0.0092093,
1651
+ 0.0195643,
1652
+ -0.0058784,
1653
+ 0.0078961,
1654
+ -0.0018558,
1655
+ -0.0332802,
1656
+ -0.0126549,
1657
+ 0.0003527,
1658
+ -0.0325215,
1659
+ 0.0357396,
1660
+ -0.0350979,
1661
+ 0.0203608,
1662
+ 0.0657294,
1663
+ -0.0410924,
1664
+ -0.0014359,
1665
+ 0.0259345,
1666
+ -0.027136,
1667
+ 0.0457944,
1668
+ -0.0290958,
1669
+ 0.0276919,
1670
+ -0.0201393,
1671
+ -0.0269499,
1672
+ -0.0059097,
1673
+ -0.238396,
1674
+ -0.012558,
1675
+ 0.0606131,
1676
+ -0.0038405,
1677
+ 0.3050316,
1678
+ 0.0053073,
1679
+ -0.0133485,
1680
+ 0.0361453,
1681
+ 0.0484401,
1682
+ -0.0515617,
1683
+ 0.0310277,
1684
+ -0.0136832,
1685
+ -0.0556593,
1686
+ -0.0321827,
1687
+ -0.0284951,
1688
+ 0.0439218,
1689
+ -0.0254445,
1690
+ -0.0127552,
1691
+ 0.0721373,
1692
+ 0.0206097,
1693
+ 0.0132098,
1694
+ -0.0089761,
1695
+ 0.0079829,
1696
+ -0.0075321,
1697
+ 0.0093124,
1698
+ -0.0187054,
1699
+ -0.0464838,
1700
+ 0.057731,
1701
+ 0.0051577,
1702
+ 0.0629364,
1703
+ 0.0032504,
1704
+ 0.0047574,
1705
+ 0.0042688,
1706
+ -0.0160958,
1707
+ 0.0298165,
1708
+ -0.0073184,
1709
+ -0.0608206,
1710
+ 0.0457134,
1711
+ 0.0437723,
1712
+ -0.0025557,
1713
+ -0.0156474,
1714
+ -0.0127475,
1715
+ 0.0342485,
1716
+ -0.0275448,
1717
+ -0.001125,
1718
+ 0.0481392,
1719
+ -0.0004659,
1720
+ 0.015135,
1721
+ 0.0579678,
1722
+ -0.0078953,
1723
+ 0.0246101,
1724
+ -0.0220383,
1725
+ 0.026813,
1726
+ 0.0231499,
1727
+ -0.0199915,
1728
+ 0.00961,
1729
+ -0.0206561,
1730
+ 0.0142155,
1731
+ 0.0067647,
1732
+ 0.0095087,
1733
+ 0.0523173,
1734
+ -0.0174483,
1735
+ 0.0398959,
1736
+ 0.0364616,
1737
+ 0.0350139,
1738
+ -0.0067026,
1739
+ -0.0409621,
1740
+ 0.0211005,
1741
+ -0.0012647,
1742
+ -0.0170568,
1743
+ -0.0003618,
1744
+ -0.0184823,
1745
+ -0.0114733,
1746
+ 0.0172884,
1747
+ 0.0080164,
1748
+ -0.0111308,
1749
+ 0.0265054,
1750
+ -0.0213923,
1751
+ -0.0088149,
1752
+ 0.0117311,
1753
+ -0.0181282,
1754
+ 0.0290931,
1755
+ 0.007055,
1756
+ -0.0008388,
1757
+ 0.0112384,
1758
+ 0.0431553,
1759
+ 0.0231443,
1760
+ 0.0070247,
1761
+ 0.0454419,
1762
+ 0.0255751,
1763
+ 0.0369707,
1764
+ -0.003059,
1765
+ -0.0032789,
1766
+ -0.0741629,
1767
+ -0.0015363,
1768
+ 0.0230501,
1769
+ 0.0400231,
1770
+ -0.0153894,
1771
+ -0.0068579,
1772
+ 0.0070991,
1773
+ -0.0205951,
1774
+ -0.0253457,
1775
+ 0.0338322,
1776
+ 0.0225516,
1777
+ 0.0175246,
1778
+ -0.0288264,
1779
+ 0.0823566,
1780
+ 0.0081965,
1781
+ -0.0106182,
1782
+ 0.0156874,
1783
+ 0.0001789,
1784
+ 0.0148504,
1785
+ 0.0117717,
1786
+ 0.0237754,
1787
+ -0.0086117,
1788
+ 0.0141754,
1789
+ 0.040231,
1790
+ -0.0331057,
1791
+ 0.0583299,
1792
+ 0.0457594,
1793
+ 0.0117246,
1794
+ 0.0342566,
1795
+ -0.0196156,
1796
+ 0.0901752,
1797
+ -0.0388455,
1798
+ -0.0651512,
1799
+ 0.0140004,
1800
+ 0.0339629,
1801
+ 0.042578,
1802
+ -0.0354294,
1803
+ 0.0235349,
1804
+ -0.0210134,
1805
+ 0.0416971,
1806
+ 0.0437426,
1807
+ -0.0424866,
1808
+ -0.0298579,
1809
+ 0.0389892,
1810
+ 0.0169898,
1811
+ -0.0264389,
1812
+ 0.034758,
1813
+ 0.0391694,
1814
+ 0.0372253,
1815
+ -0.0370816,
1816
+ -0.009757,
1817
+ -0.0709113,
1818
+ 0.0470219,
1819
+ 0.0118337,
1820
+ -0.0328658,
1821
+ -0.0113846,
1822
+ -0.0286826,
1823
+ 0.0172676,
1824
+ 0.0031711,
1825
+ -0.0201598,
1826
+ 0.0341731,
1827
+ 0.023543,
1828
+ 0.0063189,
1829
+ 0.0039108,
1830
+ 0.0083759,
1831
+ 0.0220321,
1832
+ -0.0339149,
1833
+ 0.0470052,
1834
+ 0.0067079,
1835
+ -0.0155743,
1836
+ 0.0610002,
1837
+ 0.0137803,
1838
+ -0.0104527,
1839
+ 0.0080595,
1840
+ -0.0055056,
1841
+ -0.0366666,
1842
+ 0.0390951,
1843
+ 0.0451955,
1844
+ 6.55e-05,
1845
+ -0.0554373,
1846
+ 0.0325824,
1847
+ 0.0034411,
1848
+ -0.0094541,
1849
+ 0.0053737,
1850
+ -0.0002473,
1851
+ -0.0061789,
1852
+ -0.007296,
1853
+ -0.0111808,
1854
+ -0.0176897,
1855
+ 0.0280049,
1856
+ 0.0177174,
1857
+ -0.0380043,
1858
+ -0.0497624,
1859
+ -0.0087323,
1860
+ 0.0110018,
1861
+ -0.0265812,
1862
+ 0.0269822,
1863
+ 0.0264606,
1864
+ 0.009159,
1865
+ -0.0071128,
1866
+ 0.0436609,
1867
+ 0.0021422,
1868
+ 0.0072964,
1869
+ 0.0140107,
1870
+ -0.0042789,
1871
+ 0.0438331,
1872
+ -0.0138949,
1873
+ -0.0173498,
1874
+ -0.0026295,
1875
+ -0.025416,
1876
+ 0.0098757,
1877
+ 0.0498686,
1878
+ 0.006806,
1879
+ -0.0143414,
1880
+ 0.0148724,
1881
+ -0.0585902,
1882
+ -0.0366992,
1883
+ 0.0212392,
1884
+ -0.0159478,
1885
+ -0.0019213,
1886
+ 0.0226183,
1887
+ 0.0002265,
1888
+ 0.0102534,
1889
+ 0.0026059,
1890
+ 0.043171,
1891
+ 0.0120144,
1892
+ -0.0175667,
1893
+ 0.0179332,
1894
+ -0.0314321,
1895
+ 0.0135192,
1896
+ 0.0274326,
1897
+ -0.0141854,
1898
+ -0.0160875,
1899
+ 0.0610543,
1900
+ 0.0045262,
1901
+ -0.0099408,
1902
+ -0.0283346,
1903
+ -0.1284437,
1904
+ 0.0084628,
1905
+ 0.0090422,
1906
+ -0.0450438,
1907
+ -0.0010222,
1908
+ -0.0016807,
1909
+ 0.0036075,
1910
+ -0.0028395,
1911
+ 0.0069319,
1912
+ 0.0187971,
1913
+ 0.0372987,
1914
+ -0.0047398,
1915
+ -0.0177076,
1916
+ -0.0372152,
1917
+ -0.0088774,
1918
+ 0.0449237,
1919
+ -0.031268,
1920
+ -0.0450276,
1921
+ 0.0441433,
1922
+ -0.0191669,
1923
+ 0.0403419,
1924
+ 0.0248082,
1925
+ -0.0168166,
1926
+ -0.0134252,
1927
+ -0.0117406,
1928
+ -0.0309384,
1929
+ 0.0038555,
1930
+ 0.0036268,
1931
+ -0.0663664,
1932
+ -0.0120338,
1933
+ 0.0290708,
1934
+ -0.0015897,
1935
+ 0.0098999,
1936
+ 0.0051596,
1937
+ 0.0095467,
1938
+ 0.0021275,
1939
+ 0.0100098,
1940
+ 0.0147187,
1941
+ 0.0123174,
1942
+ -0.0022185,
1943
+ -0.0318748,
1944
+ 0.0186827,
1945
+ -0.0021616,
1946
+ 0.0121651,
1947
+ -0.0037288,
1948
+ 0.003544,
1949
+ -0.0247473,
1950
+ -0.0347024,
1951
+ 0.0048945,
1952
+ -0.0175091,
1953
+ 0.2937374,
1954
+ 0.00277,
1955
+ -0.0044884,
1956
+ -0.0576349,
1957
+ -0.0149706,
1958
+ -0.0010073,
1959
+ -0.0486672,
1960
+ 0.0168386,
1961
+ -0.0088559,
1962
+ 0.0072639,
1963
+ -0.0239114,
1964
+ 0.0257517,
1965
+ -0.0026901,
1966
+ 0.018372,
1967
+ 0.0177089,
1968
+ 0.0407887,
1969
+ 0.002236,
1970
+ -0.019927,
1971
+ 0.0092718,
1972
+ -0.0381663,
1973
+ -0.031039,
1974
+ 0.0386571,
1975
+ 0.0191666,
1976
+ 0.0158292,
1977
+ 0.0121963,
1978
+ 0.0132033,
1979
+ 0.0107741,
1980
+ -0.0330748,
1981
+ -0.0244974,
1982
+ -0.0072312,
1983
+ -0.0226028,
1984
+ -0.0474324,
1985
+ 0.018386,
1986
+ 0.0055725,
1987
+ 0.0053387,
1988
+ -0.0067288,
1989
+ -0.0069327,
1990
+ 0.0388352,
1991
+ -0.0092138,
1992
+ 0.0017214,
1993
+ 0.0108645,
1994
+ -0.007365,
1995
+ -0.0308875,
1996
+ 0.0276345,
1997
+ 0.0030492,
1998
+ 0.0597437,
1999
+ 0.0149274,
2000
+ 0.0108632,
2001
+ 0.0063664,
2002
+ -0.0472365,
2003
+ -0.0659725,
2004
+ 0.0116199,
2005
+ -0.0311121,
2006
+ -0.0084875,
2007
+ 0.021202,
2008
+ -0.0144852,
2009
+ -0.0276581,
2010
+ 0.0346197,
2011
+ 0.0190425,
2012
+ 0.0021423,
2013
+ -0.0214823,
2014
+ 0.0042432,
2015
+ 0.0367772,
2016
+ -0.0352249,
2017
+ -0.0175849,
2018
+ -0.0085295,
2019
+ -0.0160579,
2020
+ -0.0221073,
2021
+ 0.0715203,
2022
+ -0.022603,
2023
+ 0.0121224,
2024
+ -0.0373269,
2025
+ 0.0218277,
2026
+ 0.0170323,
2027
+ -0.0286245,
2028
+ -0.0078001,
2029
+ 0.0256803,
2030
+ -0.0148012,
2031
+ 0.0099331,
2032
+ -0.0136366,
2033
+ 0.0587975,
2034
+ -0.0419719,
2035
+ -0.0031683,
2036
+ 0.0040288,
2037
+ -0.0167476,
2038
+ 0.0084212,
2039
+ 0.0180768,
2040
+ -0.0465007,
2041
+ -0.0154712,
2042
+ 0.0111867,
2043
+ -0.0109722,
2044
+ -0.0218118,
2045
+ -0.0080581,
2046
+ 0.0726468,
2047
+ -0.0298194,
2048
+ 0.0206009,
2049
+ -0.0122841,
2050
+ 0.0012644,
2051
+ -0.0214854,
2052
+ 0.020771,
2053
+ 0.0373861,
2054
+ -0.0202912,
2055
+ 0.0086824,
2056
+ 0.0104239,
2057
+ -0.0177915,
2058
+ -0.0195416,
2059
+ -0.0051046,
2060
+ 0.00611,
2061
+ 0.0131852,
2062
+ -0.0365019,
2063
+ 0.0227813,
2064
+ 0.0248264,
2065
+ -0.0207044,
2066
+ -0.0116764,
2067
+ 0.0259135,
2068
+ 0.0267124,
2069
+ -0.1291034,
2070
+ 0.0239644,
2071
+ 0.0625211,
2072
+ 0.0244442,
2073
+ 0.0325541,
2074
+ 0.0351543,
2075
+ 0.0217503,
2076
+ 0.0121765,
2077
+ 0.0449031,
2078
+ 0.0567871,
2079
+ 0.0054928,
2080
+ 0.0165023,
2081
+ -0.0382681,
2082
+ 0.0124143,
2083
+ 0.037944,
2084
+ 0.0284664,
2085
+ -0.0216116,
2086
+ 0.0124165,
2087
+ 0.0038941,
2088
+ -0.0059169,
2089
+ -0.0400646,
2090
+ -0.0100921,
2091
+ -0.0168236,
2092
+ 0.0026536,
2093
+ 0.0008066,
2094
+ 0.0205489,
2095
+ 0.0077813,
2096
+ -0.0524134,
2097
+ -0.0034027,
2098
+ -0.005347,
2099
+ -0.0321935,
2100
+ 0.0125179,
2101
+ -0.0294378,
2102
+ 0.0317069,
2103
+ 0.010248,
2104
+ -0.0071578,
2105
+ -0.0060076,
2106
+ -0.0125832,
2107
+ -0.0200377,
2108
+ 0.0289566,
2109
+ -0.0045058,
2110
+ -0.0214601,
2111
+ 0.0466605,
2112
+ -0.0174458,
2113
+ -0.037283,
2114
+ 0.0125868,
2115
+ -0.0232067,
2116
+ -0.2290279,
2117
+ 0.0035629,
2118
+ -0.0056789,
2119
+ 0.0374352,
2120
+ -0.0616575,
2121
+ -0.0058487,
2122
+ 0.0131172,
2123
+ 0.0539453,
2124
+ -0.0240027,
2125
+ -0.0390158,
2126
+ 0.0107827,
2127
+ 0.0038621,
2128
+ -0.0261863,
2129
+ 0.0437467,
2130
+ -0.0608736,
2131
+ -0.0023836,
2132
+ 0.0005415,
2133
+ 0.0291586,
2134
+ 0.0035115,
2135
+ -0.000339,
2136
+ 0.0223815,
2137
+ -0.0281816,
2138
+ 0.0592663,
2139
+ -0.007491,
2140
+ 0.0226631,
2141
+ 0.0274635,
2142
+ -0.0112232,
2143
+ -0.0176956,
2144
+ -0.021663,
2145
+ -0.0004892,
2146
+ -0.0213772,
2147
+ -0.0075747,
2148
+ -0.0309256,
2149
+ -0.0192184,
2150
+ 0.0389742,
2151
+ 0.0291572,
2152
+ 0.0186152,
2153
+ -0.0461713,
2154
+ 0.020704,
2155
+ -0.0140283,
2156
+ -0.0298755,
2157
+ -0.0019759,
2158
+ 0.0245542,
2159
+ -0.017876,
2160
+ -0.000494,
2161
+ -0.0161591,
2162
+ 0.0552246,
2163
+ 0.0046217,
2164
+ 0.0244851,
2165
+ -0.0494911,
2166
+ 0.0113249,
2167
+ -0.0002698,
2168
+ 0.022198,
2169
+ 0.0076108,
2170
+ 0.0061694,
2171
+ 0.0151072,
2172
+ 9.93e-05,
2173
+ 0.0397179,
2174
+ 0.0182748,
2175
+ -0.0288742,
2176
+ -0.0532453,
2177
+ 0.0270052,
2178
+ 0.038392,
2179
+ -0.001379,
2180
+ -0.0237011,
2181
+ 0.0293171,
2182
+ 0.004786,
2183
+ 0.0394925,
2184
+ 0.0042305,
2185
+ 0.0089678,
2186
+ 0.0208281,
2187
+ 0.0092142,
2188
+ -0.0111049,
2189
+ 0.0429073,
2190
+ -0.0086474,
2191
+ 0.004501,
2192
+ -0.042835,
2193
+ -0.0161324,
2194
+ 0.0090948,
2195
+ -0.0291586,
2196
+ -0.0044904,
2197
+ -0.0001088,
2198
+ 0.0094551,
2199
+ -0.0286622,
2200
+ -0.0575663,
2201
+ 0.0294203,
2202
+ -0.0009848,
2203
+ -0.0122502,
2204
+ -0.0377292,
2205
+ 0.0191204,
2206
+ 0.0062761,
2207
+ 0.0521794,
2208
+ -0.0036846,
2209
+ 0.0403789,
2210
+ -0.0335401,
2211
+ 0.0054602,
2212
+ -0.0295457,
2213
+ 0.0062646,
2214
+ -0.0318549,
2215
+ -0.0200798,
2216
+ -0.0350457,
2217
+ -0.0211371,
2218
+ -0.0045081,
2219
+ -0.0536032,
2220
+ 0.0180079,
2221
+ 0.0292347,
2222
+ -0.0373302,
2223
+ 0.0102212,
2224
+ -0.0161656,
2225
+ 0.0030769,
2226
+ -0.0042317,
2227
+ 0.0422946,
2228
+ -0.0132999,
2229
+ 0.0639726,
2230
+ 0.0063302,
2231
+ 0.0127261,
2232
+ 0.0608007,
2233
+ 0.0217346,
2234
+ 0.0141945,
2235
+ -0.0055504,
2236
+ 0.0164727,
2237
+ 0.0310062,
2238
+ 0.0074169,
2239
+ 0.0124027,
2240
+ 0.0103245,
2241
+ -0.0233224,
2242
+ -0.0102762,
2243
+ 0.0091786,
2244
+ 0.0063271,
2245
+ -0.0013132,
2246
+ 0.0032481,
2247
+ 0.0331999,
2248
+ 0.0064559,
2249
+ -0.0219145,
2250
+ 0.0096493,
2251
+ 0.0178552,
2252
+ 0.030541,
2253
+ 0.0063936,
2254
+ 0.0139802,
2255
+ -0.0252062,
2256
+ 0.0227818,
2257
+ -0.0051253,
2258
+ -0.0292184,
2259
+ -0.0409617,
2260
+ 0.022986,
2261
+ -0.0172845,
2262
+ 0.0212696,
2263
+ -0.021213,
2264
+ -0.0138542,
2265
+ -0.0126601,
2266
+ -0.0104976,
2267
+ -0.020326,
2268
+ 0.0113664,
2269
+ -0.0096861,
2270
+ 0.0007517,
2271
+ 0.0258834,
2272
+ -0.0228422,
2273
+ -0.0242639,
2274
+ -0.0164893,
2275
+ -0.0155036,
2276
+ -0.0012227,
2277
+ -0.0143082,
2278
+ -0.0010508,
2279
+ 0.0134848,
2280
+ -0.0181285,
2281
+ -0.0109053,
2282
+ -0.0121414,
2283
+ -0.0269855,
2284
+ -0.0168856,
2285
+ 0.0062585,
2286
+ 0.0075284,
2287
+ -0.0223102,
2288
+ 0.0206764,
2289
+ -0.0381524,
2290
+ -0.0254449,
2291
+ 0.0046192,
2292
+ 0.0094333,
2293
+ -0.0023871,
2294
+ 0.1939883,
2295
+ -0.010786,
2296
+ -0.0109085,
2297
+ -0.0034179,
2298
+ -0.0061638,
2299
+ -0.0228264,
2300
+ 0.0092627,
2301
+ 0.0038894,
2302
+ -0.0105737,
2303
+ 0.0254012,
2304
+ -0.0387676,
2305
+ 0.0177953,
2306
+ 0.0114002,
2307
+ -0.0082272,
2308
+ -0.0021445,
2309
+ 0.0136881,
2310
+ -0.0476875,
2311
+ 0.0161518,
2312
+ -0.0005517,
2313
+ 0.0211178,
2314
+ 0.0158259,
2315
+ 0.0371481,
2316
+ 0.0041266,
2317
+ 0.0420225,
2318
+ 0.0057804,
2319
+ 0.0119182,
2320
+ 0.00443,
2321
+ -0.0013008,
2322
+ -0.001863,
2323
+ 0.0110961,
2324
+ 0.0062314,
2325
+ -0.0355586,
2326
+ -0.0639177,
2327
+ 0.0010688,
2328
+ 0.0185759,
2329
+ 0.0168337
2330
+ ]
2331
+ }
2332
+ ]
2333
+ }
tests/tokenizer-cases.json ADDED
The diff for this file is too large to render. See raw diff