Views
No views yet
transformers, or the paper's research package.model2onnx.py
from the Vespa sample app.Why this exists. The upstream checkpoints cannot be loaded bytransformers>=5:Hypencoder.__init__callsAutoModel.from_pretrained()inside the constructor, and transformers 5 wrapscls(config)in a meta-device context, so the nested load is rejected bycheck_and_set_device_map. Exporting once and shipping ONNX removestorch,transformers, and the editablehypencoder-paperinstall requirement for running our sample app.
| directory | q-net blocks | query-encoder outputs | generated params per query | passage_encoder | query_encoder |
|---|---|---|---|---|---|
2_layer/ | 2 | 5 | 1,181,952 | 435.8 MB | 480.8 MB |
4_layer/ | 4 | 9 | 2,363,136 | 435.8 MB | 518.6 MB |
6_layer/ | 6 | 13 | 3,544,320 | 435.8 MB | 556.4 MB |
8_layer/ | 8 | 17 | 4,725,504 | 435.8 MB | 594.3 MB |
passage_encoder.onnx, query_encoder.onnx and tokenizer.json.passage_encoder.onnx — input_ids, attention_mask (int64, [batch, seq])
→ last_hidden_state [batch, seq, 768]. CLS-pool it (take index 0) and do not
L2-normalise.query_encoder.onnx — input_ids, attention_mask (float32, [batch, seq])
→ W0 [768,768], b0 [768], … , W{n-1}, b{n-1}, Wout [768].(out, in) so
they arrive in Vespa's alphabetical dimension order.linear → ReLU → parameter-free LayerNorm, with a residual
connection, followed by a final linear projection to a scalar. For the 2-block model, as
written in the Vespa sample app's rank profile:1import numpy as np
2
3def layer_norm(v, eps=1e-5):
4 return (v - v.mean(-1, keepdims=True)) / np.sqrt(v.var(-1, keepdims=True) + eps)
5
6def score_2layer(x0, W0, b0, W1, b1, Wout):
7 relu0 = np.maximum(x0 @ W0.T + b0, 0.0)
8 res0 = layer_norm(relu0) + x0 # residual on the first block
9 relu1 = np.maximum(res0 @ W1.T + b1, 0.0)
10 return layer_norm(relu1) @ Wout # no residual before the final projectionq_net.py
as authoritative for where residuals and layer norms are applied — the snippet above is
written for the 2-block case only.1import numpy as np, onnxruntime as ort
2from tokenizers import Tokenizer
3
4D = "2_layer"
5tok = Tokenizer.from_file(f"{D}/tokenizer.json")
6enc = ort.InferenceSession(f"{D}/passage_encoder.onnx")
7qenc = ort.InferenceSession(f"{D}/query_encoder.onnx")
8
9d = tok.encode("Mount Everest is Earth's highest mountain, at 8,849 metres.")
10doc_vec = enc.run(None, {"input_ids": np.array([d.ids], dtype=np.int64),
11 "attention_mask": np.array([d.attention_mask], dtype=np.int64)}
12 )[0][:, 0] # CLS pooling
13
14q = tok.encode("tallest mountain in the world")
15names = [o.name for o in qenc.get_outputs()]
16w = dict(zip(names, qenc.run(None, {
17 "input_ids": np.array([q.ids], dtype=np.float32), # float, not int64
18 "attention_mask": np.array([q.attention_mask], dtype=np.float32)})))
19
20print(score_2layer(doc_vec.astype(np.float64),
21 w["W0"], w["b0"], w["W1"], w["b1"], w["Wout"]))1<component id="passage_embedder" type="hugging-face-embedder">
2 <transformer-model url="https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/passage_encoder.onnx"/>
3 <tokenizer-model url="https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/tokenizer.json"/>
4 <pooling-strategy>cls</pooling-strategy>
5 <normalize>false</normalize>
6</component>onnx-model does not accept a URI
(OnnxModel.setUri() throws "URI for ONNX models are not currently supported"), so
download it into the package first:1mkdir -p app/models
2curl -L -o app/models/query_encoder.onnx \
3 https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/query_encoder.onnxonnx-model query_encoder {
file: models/query_encoder.onnx
input "input_ids": query(input_ids)
input "attention_mask": query(attention_mask)
}4_layer, 6_layer or
8_layer means extending that expression with the extra W{i}/b{i} blocks — the ONNX
outputs are there, but the ranking expression is not written for them.input_ids/attention_mask to int64 internally, (c) pre-transposes weight matrices
to (out, in), and (d) uses static layer-norm shapes so the legacy TorchScript exporter
inlines weights into a single file rather than an external .data sidecar.1@inproceedings{killingback2025hypencoder,
2 title = {Hypencoder: Hypernetworks for Information Retrieval},
3 author = {Killingback, Julian and Zeng, Hansi and Zamani, Hamed},
4 booktitle = {SIGIR},
5 year = {2025}
6}