andreer commited on
Commit
e0bd82d
Β·
verified Β·
1 Parent(s): a938107

fp32 ONNX exports of all four Hypencoder checkpoints

Browse files
2_layer/passage_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5f59e6bfd7d89658d8cfee2133d3a7b5e45a302fd15c042019ed43692ba0df1c
3
+ size 435860222
2_layer/query_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02b284a3e0a9e24c251bcecf7480c148c8c3a7d3f195a3669e532e56aa439bbf
3
+ size 480834359
2_layer/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
4_layer/passage_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ed883b26da4e62aa932fc854826729f1034136023a59a69527c53e0d5d50b1f
3
+ size 435860222
4_layer/query_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca96639b56c684450e7f23746b0be2cbb4cb2dab9f3ec827748f451210cdd902
3
+ size 518659573
4_layer/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
6_layer/passage_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a5c339faf41279f1e9489834d24eebfd3386b58b740a64c7643110d2b4f8d97
3
+ size 435860222
6_layer/query_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b526006e7b7c5298d08d6322cde553fb88fd35cdf10195793ebfd0231f207789
3
+ size 556484953
6_layer/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
8_layer/passage_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a0325451dcfb622ad8989d8397284d968a5b5488e98061cc74646addcab07129
3
+ size 435860222
8_layer/query_encoder.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:076395b8413001bf8c1d87d6724abc901b45c0ff6f932014e7d2ab7b8e8eb794
3
+ size 594310387
8_layer/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
README.md ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model:
4
+ - jfkback/hypencoder.2_layer
5
+ - jfkback/hypencoder.4_layer
6
+ - jfkback/hypencoder.6_layer
7
+ - jfkback/hypencoder.8_layer
8
+ tags:
9
+ - onnx
10
+ - information-retrieval
11
+ - vespa
12
+ - hypencoder
13
+ library_name: onnx
14
+ ---
15
+
16
+ # Hypencoder β€” ONNX exports (fp32)
17
+
18
+ Ready-to-run fp32 ONNX artifacts for all four released
19
+ [Hypencoder](https://arxiv.org/abs/2502.05364) checkpoints, so you can run them without
20
+ PyTorch, `transformers`, or the paper's research package.
21
+
22
+ A Hypencoder replaces the fixed inner product of a bi-encoder with a *learned,
23
+ query-specific* neural network: a hypernetwork turns the query into a small MLP (a
24
+ "q-net") whose input is a document vector and whose output is the relevance score.
25
+
26
+ Exported from the upstream checkpoints (all Apache-2.0) with
27
+ [`model2onnx.py`](https://github.com/vespa-engine/sample-apps/blob/master/hypencoder/model2onnx.py)
28
+ from the Vespa sample app.
29
+
30
+ > **Why this exists.** The upstream checkpoints cannot be loaded by `transformers>=5`:
31
+ > `Hypencoder.__init__` calls `AutoModel.from_pretrained()` *inside the constructor*, and
32
+ > transformers 5 wraps `cls(config)` in a meta-device context, so the nested load is
33
+ > rejected by `check_and_set_device_map`. Exporting once and shipping ONNX removes
34
+ > `torch`, `transformers`, and the editable `hypencoder-paper` install requirement for running our sample app.
35
+
36
+ ## Contents
37
+
38
+ | directory | q-net blocks | query-encoder outputs | generated params per query | passage_encoder | query_encoder |
39
+ |---|---|---|---|---|---|
40
+ | `2_layer/` | 2 | 5 | 1,181,952 | 435.8 MB | 480.8 MB |
41
+ | `4_layer/` | 4 | 9 | 2,363,136 | 435.8 MB | 518.6 MB |
42
+ | `6_layer/` | 6 | 13 | 3,544,320 | 435.8 MB | 556.4 MB |
43
+ | `8_layer/` | 8 | 17 | 4,725,504 | 435.8 MB | 594.3 MB |
44
+
45
+ Each directory holds `passage_encoder.onnx`, `query_encoder.onnx` and `tokenizer.json`.
46
+
47
+ The four passage encoders are **not** interchangeable β€” each checkpoint is a separate
48
+ fine-tune, so the document towers differ. Pair a passage encoder only
49
+ with the query encoder from the same directory.
50
+
51
+ ## I/O contract
52
+
53
+ Identical across depths except for the number of query-encoder outputs.
54
+
55
+ **`passage_encoder.onnx`** β€” `input_ids`, `attention_mask` (**int64**, `[batch, seq]`)
56
+ β†’ `last_hidden_state` `[batch, seq, 768]`. **CLS-pool it** (take index 0) and do **not**
57
+ L2-normalise.
58
+
59
+ **`query_encoder.onnx`** β€” `input_ids`, `attention_mask` (**float32**, `[batch, seq]`)
60
+ β†’ `W0` `[768,768]`, `b0` `[768]`, … , `W{n-1}`, `b{n-1}`, `Wout` `[768]`.
61
+
62
+ Inputs are float32 rather than int64 because Vespa tensors have no int64 cell type; the
63
+ export wrapper casts internally. Weight matrices are pre-transposed to `(out, in)` so
64
+ they arrive in Vespa's alphabetical dimension order.
65
+
66
+ ## Scoring
67
+
68
+ Each q-net block is `linear β†’ ReLU β†’ parameter-free LayerNorm`, with a residual
69
+ connection, followed by a final linear projection to a scalar. For the 2-block model, as
70
+ written in the Vespa sample app's rank profile:
71
+
72
+ ```python
73
+ import numpy as np
74
+
75
+ def layer_norm(v, eps=1e-5):
76
+ return (v - v.mean(-1, keepdims=True)) / np.sqrt(v.var(-1, keepdims=True) + eps)
77
+
78
+ def score_2layer(x0, W0, b0, W1, b1, Wout):
79
+ relu0 = np.maximum(x0 @ W0.T + b0, 0.0)
80
+ res0 = layer_norm(relu0) + x0 # residual on the first block
81
+ relu1 = np.maximum(res0 @ W1.T + b1, 0.0)
82
+ return layer_norm(relu1) @ Wout # no residual before the final projection
83
+ ```
84
+
85
+ For the deeper checkpoints, treat
86
+ [`q_net.py`](https://github.com/jfkback/hypencoder-paper/blob/main/hypencoder_cb/modeling/q_net.py)
87
+ as authoritative for where residuals and layer norms are applied β€” the snippet above is
88
+ written for the 2-block case only.
89
+
90
+ ## Usage with onnxruntime
91
+
92
+ ```python
93
+ import numpy as np, onnxruntime as ort
94
+ from tokenizers import Tokenizer
95
+
96
+ D = "2_layer"
97
+ tok = Tokenizer.from_file(f"{D}/tokenizer.json")
98
+ enc = ort.InferenceSession(f"{D}/passage_encoder.onnx")
99
+ qenc = ort.InferenceSession(f"{D}/query_encoder.onnx")
100
+
101
+ d = tok.encode("Mount Everest is Earth's highest mountain, at 8,849 metres.")
102
+ doc_vec = enc.run(None, {"input_ids": np.array([d.ids], dtype=np.int64),
103
+ "attention_mask": np.array([d.attention_mask], dtype=np.int64)}
104
+ )[0][:, 0] # CLS pooling
105
+
106
+ q = tok.encode("tallest mountain in the world")
107
+ names = [o.name for o in qenc.get_outputs()]
108
+ w = dict(zip(names, qenc.run(None, {
109
+ "input_ids": np.array([q.ids], dtype=np.float32), # float, not int64
110
+ "attention_mask": np.array([q.attention_mask], dtype=np.float32)})))
111
+
112
+ print(score_2layer(doc_vec.astype(np.float64),
113
+ w["W0"], w["b0"], w["W1"], w["b1"], w["Wout"]))
114
+ ```
115
+
116
+ ## Usage with Vespa
117
+
118
+ The passage encoder and tokenizer can be fetched **by URL at deploy time**, so they never
119
+ enter your application package:
120
+
121
+ ```xml
122
+ <component id="passage_embedder" type="hugging-face-embedder">
123
+ <transformer-model url="https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/passage_encoder.onnx"/>
124
+ <tokenizer-model url="https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/tokenizer.json"/>
125
+ <pooling-strategy>cls</pooling-strategy>
126
+ <normalize>false</normalize>
127
+ </component>
128
+ ```
129
+
130
+ The query encoder **must be a local file** β€” `onnx-model` does not accept a URI
131
+ (`OnnxModel.setUri()` throws "URI for ONNX models are not currently supported"), so
132
+ download it into the package first:
133
+
134
+ ```sh
135
+ mkdir -p app/models
136
+ curl -L -o app/models/query_encoder.onnx \
137
+ https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/query_encoder.onnx
138
+ ```
139
+
140
+ ```
141
+ onnx-model query_encoder {
142
+ file: models/query_encoder.onnx
143
+ input "input_ids": query(input_ids)
144
+ input "attention_mask": query(attention_mask)
145
+ }
146
+ ```
147
+
148
+ **The sample app's rank profile implements 2 blocks only.** Using `4_layer`, `6_layer` or
149
+ `8_layer` means extending that expression with the extra `W{i}`/`b{i}` blocks β€” the ONNX
150
+ outputs are there, but the ranking expression is not written for them.
151
+
152
+ ## Modifications relative to the base models
153
+
154
+ Per Apache-2.0 Β§4(b): no weights were retrained or altered numerically. The export
155
+ wrapper (a) returns the q-net weight tensors directly instead of a callable, (b) casts
156
+ float `input_ids`/`attention_mask` to int64 internally, (c) pre-transposes weight matrices
157
+ to `(out, in)`, and (d) uses static layer-norm shapes so the legacy TorchScript exporter
158
+ inlines weights into a single file rather than an external `.data` sidecar.
159
+
160
+ ## Citation
161
+
162
+ ```bibtex
163
+ @inproceedings{killingback2025hypencoder,
164
+ title = {Hypencoder: Hypernetworks for Information Retrieval},
165
+ author = {Killingback, Julian and Zeng, Hansi and Zamani, Hamed},
166
+ booktitle = {SIGIR},
167
+ year = {2025}
168
+ }
169
+ ```
170
+
171
+ Weights Β© the original authors, Apache-2.0. This repository redistributes them in ONNX
172
+ form under the same licence.