thomasht86 commited on
Commit
7dd7a5b
·
verified ·
1 Parent(s): ec65cca

Text-only ONNX (fp32 + int8) of google/embeddinggemma-2 for Vespa

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ 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
+ onnx/int8/model.onnx_data filter=lfs diff=lfs merge=lfs -text
37
+ onnx/model.onnx_data filter=lfs diff=lfs merge=lfs -text
38
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ pipeline_tag: feature-extraction
4
+ library_name: onnx
5
+ base_model:
6
+ - google/embeddinggemma-2
7
+ - onnx-community/embeddinggemma-2-ONNX
8
+ tags:
9
+ - embedding
10
+ - sentence-similarity
11
+ - vespa
12
+ ---
13
+
14
+ # EmbeddingGemma 2 — text-only ONNX (for Vespa)
15
+
16
+ Text-only ONNX export of [google/embeddinggemma-2](https://huggingface.co/google/embeddinggemma-2),
17
+ derived from [onnx-community/embeddinggemma-2-ONNX](https://huggingface.co/onnx-community/embeddinggemma-2-ONNX)
18
+ for use with Vespa's [`hugging-face-embedder`](https://docs.vespa.ai/en/rag/embedding.html#huggingface-embedder).
19
+
20
+ The upstream graph requires `image_features`, `video_features` and `audio_features` inputs that are
21
+ concatenated after the text tokens. These are replaced with empty `[0, 512]` constants, so the
22
+ graph takes only `input_ids` and `attention_mask`. Weights are unchanged. See `scripts/make_text_only.py`.
23
+
24
+ | File | Precision | Size |
25
+ | --- | --- | --- |
26
+ | `onnx/model.onnx` (+ `model.onnx_data`) | fp32 | 1.08 GB |
27
+ | `onnx/int8/model.onnx` (+ `model.onnx_data`) | int8 (dynamic quantization) | 314 MB |
28
+
29
+ Outputs: `last_hidden_state` `[batch, seq, 768]` (use mean pooling + L2 normalize) and `sentence_embedding` `[batch, 768]`.
30
+
31
+ Do **not** use fp16. Per the model card, activations overflow float16.
32
+
33
+ ## Verification
34
+
35
+ `scripts/verify.py` compares Vespa-style inference (tokenizer.json with special tokens, mean pooling over
36
+ `last_hidden_state`, L2 normalization) against `sentence-transformers` 6.1 / transformers 5.19 in fp32,
37
+ on 11 texts including multilingual, code, and a 2,718-token document (exercises sliding-window attention).
38
+ Token IDs are identical to the reference.
39
+
40
+ | Variant | Cosine vs. reference (min, 768d / 512d / 256d / 128d) | Same ranking |
41
+ | --- | --- | --- |
42
+ | fp32 | 1.000000 / 1.000000 / 1.000000 / 1.000000 (max abs diff 6e-7) | yes |
43
+ | int8 | 0.999896 / 0.999901 / 0.999910 / 0.999940 | yes |
44
+
45
+ ## Vespa usage
46
+
47
+ ```xml
48
+ <component id="embeddinggemma2" type="hugging-face-embedder">
49
+ <transformer-model url="https://huggingface.co/vespa-engine/embeddinggemma-2-ONNX/resolve/main/onnx/model.onnx"/>
50
+ <tokenizer-model url="https://huggingface.co/vespa-engine/embeddinggemma-2-ONNX/resolve/main/tokenizer.json"/>
51
+ <max-tokens>8192</max-tokens>
52
+ <pooling-strategy>mean</pooling-strategy>
53
+ <normalize>true</normalize>
54
+ <prepend>
55
+ <query>task: search result | query: </query>
56
+ <document>title: none | text: </document>
57
+ </prepend>
58
+ </component>
59
+ ```
60
+
61
+ Supports Matryoshka truncation to 512, 256 and 128 dimensions (re-normalize after truncating).
onnx/int8/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d6ee6b22d9c48d013d9c931d52f7261d73b29cdb4b255264aaa27200d5e8fb1b
3
+ size 488970
onnx/int8/model.onnx_data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:278a7ff1248c3618e4bd11a607fc54f7bdc7778854230f3956d3f86bd9db4f3b
3
+ size 313724928
onnx/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cbd21b1b6ea4f47ef4e8789d138c27d396d14a369cc7b7eb4855f360d0fac563
3
+ size 421349
onnx/model.onnx_data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fd452bfc6916e5a92f080f8dada35598ca4512b21c65897d4c5974d5f9630a2
3
+ size 1084170240
scripts/make_text_only.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Make onnx-community/embeddinggemma-2-ONNX text-only for Vespa's hugging-face-embedder.
2
+
3
+ The upstream graph requires image_features/video_features/audio_features inputs
4
+ (each [num_tokens, 512]) that are concatenated after the text embeddings.
5
+ For text-only inference these are empty, so we replace them with constant
6
+ [0, 512] initializers. The weights are untouched: each variant is written as
7
+ <out>/<variant>/model.onnx referencing model.onnx_data (hard-linked to the
8
+ upstream data file), matching how modelhub/Vespa store external data.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import numpy as np
16
+ import onnx
17
+ from onnx import numpy_helper
18
+
19
+ SRC = Path(sys.argv[1]) # onnx-community snapshot dir
20
+ OUT = Path(sys.argv[2])
21
+ VARIANTS = {"fp32": "model", "int8": "model_quantized", "q4": "model_q4"}
22
+ MEDIA_INPUTS = ["image_features", "video_features", "audio_features"]
23
+
24
+ for variant, name in VARIANTS.items():
25
+ dst = OUT / variant
26
+ dst.mkdir(parents=True, exist_ok=True)
27
+ m = onnx.load(SRC / "onnx" / f"{name}.onnx", load_external_data=False)
28
+ for inp in [i for i in m.graph.input if i.name in MEDIA_INPUTS]:
29
+ dtype = onnx.helper.tensor_dtype_to_np_dtype(inp.type.tensor_type.elem_type)
30
+ width = inp.type.tensor_type.shape.dim[1].dim_value
31
+ m.graph.input.remove(inp)
32
+ m.graph.initializer.append(
33
+ numpy_helper.from_array(np.zeros((0, width), dtype=dtype), inp.name)
34
+ )
35
+ for t in m.graph.initializer:
36
+ for e in t.external_data:
37
+ if e.key == "location":
38
+ assert e.value == f"{name}.onnx_data", e.value
39
+ e.value = "model.onnx_data"
40
+ onnx.save_model(m, dst / "model.onnx")
41
+ data = dst / "model.onnx_data"
42
+ data.unlink(missing_ok=True)
43
+ os.link(os.path.realpath(SRC / "onnx" / f"{name}.onnx_data"), data)
44
+ # onnx.checker rejects hard-linked data files; we validate by loading in ORT instead.
45
+ print(variant, [i.name for i in m.graph.input], [o.name for o in m.graph.output])
scripts/verify.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify text-only ONNX variants against the sentence-transformers fp32 reference.
2
+
3
+ ONNX side mimics Vespa's hugging-face-embedder: tokenizer.json via `tokenizers`
4
+ (add_special_tokens=True), batch size 1, mean pooling over last_hidden_state with
5
+ the attention mask, then L2 normalize (and truncate+renormalize for MRL dims).
6
+ """
7
+
8
+ import sys
9
+
10
+ import numpy as np
11
+ import onnxruntime as ort
12
+ import torch
13
+ from sentence_transformers import SentenceTransformer
14
+ from tokenizers import Tokenizer
15
+
16
+ OUT = sys.argv[1]
17
+ TOKENIZER = sys.argv[2]
18
+ Q = "task: search result | query: "
19
+ D = "title: none | text: "
20
+
21
+ long_doc = " ".join(
22
+ f"Paragraph {i}: The aurora borealis appears when charged solar particles collide with "
23
+ f"gases in Earth's upper atmosphere, producing light in green, red and violet hues."
24
+ for i in range(80)
25
+ )
26
+ texts = [
27
+ Q + "What causes the northern lights?",
28
+ D + "The northern lights are caused by charged particles from the sun.",
29
+ Q + "Which planet is known as the Red Planet?",
30
+ D + "Mars, known for its reddish appearance, is often referred to as the Red Planet.",
31
+ D + "Venus is often called Earth's twin because of its similar size and proximity.",
32
+ Q + "Hvordan lager man brunost?",
33
+ D + "Brunost lages ved å koke myse til sukkeret karamelliseres. 日本語のテキストも含む。",
34
+ "task: code retrieval | query: reverse a linked list in python",
35
+ D + "def reverse(head):\n prev = None\n while head:\n head.next, prev, head = prev, head, head.next\n return prev",
36
+ "a",
37
+ D + long_doc,
38
+ ]
39
+
40
+ st = SentenceTransformer(
41
+ "google/embeddinggemma-2",
42
+ model_kwargs={"torch_dtype": torch.float32},
43
+ config_kwargs={"vision_config": None, "audio_config": None},
44
+ )
45
+ with torch.no_grad():
46
+ ref = st.encode(texts, normalize_embeddings=True, batch_size=1, convert_to_numpy=True)
47
+
48
+ tok = Tokenizer.from_file(TOKENIZER)
49
+ st_ids = [st.tokenize([t])["input_ids"][0].tolist() for t in texts]
50
+ vespa_ids = [tok.encode(t, add_special_tokens=True).ids for t in texts]
51
+ print("token ids identical:", all(a == b for a, b in zip(st_ids, vespa_ids)))
52
+ print("long doc tokens:", len(vespa_ids[-1]), "| sample ids:", vespa_ids[0][:4], "...", vespa_ids[0][-2:])
53
+
54
+
55
+ def norm(x):
56
+ return x / np.linalg.norm(x, axis=-1, keepdims=True)
57
+
58
+
59
+ for variant in ["fp32", "int8", "q4"]:
60
+ sess = ort.InferenceSession(f"{OUT}/{variant}/model.onnx", providers=["CPUExecutionProvider"])
61
+ embs, sent = [], []
62
+ for ids in vespa_ids:
63
+ ii = np.array([ids], dtype=np.int64)
64
+ am = np.ones_like(ii)
65
+ lhs, se = sess.run(["last_hidden_state", "sentence_embedding"], {"input_ids": ii, "attention_mask": am})
66
+ embs.append((lhs[0] * am[0, :, None]).sum(0) / am.sum())
67
+ sent.append(se[0])
68
+ embs = norm(np.stack(embs))
69
+ sent = norm(np.stack(sent))
70
+ print(f"\n== {variant}")
71
+ print(" any NaN:", bool(np.isnan(embs).any()))
72
+ print(" meanpool vs sentence_embedding output, min cos:", float((embs * sent).sum(-1).min()))
73
+ for dim in [768, 512, 256, 128]:
74
+ a, b = norm(embs[:, :dim]), norm(ref[:, :dim])
75
+ cos = (a * b).sum(-1)
76
+ print(f" dim {dim}: cos vs ST min={cos.min():.6f} mean={cos.mean():.6f} | max abs diff={np.abs(a - b).max():.2e}")
77
+ # Retrieval sanity: q0->d1, q2->d3 should rank first among docs; compare score matrices
78
+ qi, di = [0, 2, 5, 7], [1, 3, 4, 6, 8]
79
+ s_onnx, s_ref = embs[qi] @ embs[di].T, ref[qi] @ ref[di].T
80
+ print(" ranking identical to ST:", bool((s_onnx.argsort(1) == s_ref.argsort(1)).all()),
81
+ "| max score diff:", f"{np.abs(s_onnx - s_ref).max():.2e}")
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4d777ef5bdc1aa36227abdfb77c3e49e7b9c892d16e1b6bda41c393504828be4
3
+ size 32170510
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
+ }