--- license: apache-2.0 base_model: - jfkback/hypencoder.2_layer - jfkback/hypencoder.4_layer - jfkback/hypencoder.6_layer - jfkback/hypencoder.8_layer tags: - onnx - information-retrieval - vespa - hypencoder library_name: onnx --- # Hypencoder — ONNX exports (fp32) Ready-to-run fp32 ONNX artifacts for all four released [Hypencoder](https://arxiv.org/abs/2502.05364) checkpoints, so you can run them without PyTorch, `transformers`, or the paper's research package. A Hypencoder replaces the fixed inner product of a bi-encoder with a *learned, query-specific* neural network: a hypernetwork turns the query into a small MLP (a "q-net") whose input is a document vector and whose output is the relevance score. Exported from the upstream checkpoints (all Apache-2.0) with [`model2onnx.py`](https://github.com/vespa-engine/sample-apps/blob/master/hypencoder/model2onnx.py) from the Vespa sample app. > **Why this exists.** The upstream checkpoints cannot be loaded by `transformers>=5`: > `Hypencoder.__init__` calls `AutoModel.from_pretrained()` *inside the constructor*, and > transformers 5 wraps `cls(config)` in a meta-device context, so the nested load is > rejected by `check_and_set_device_map`. Exporting once and shipping ONNX removes > `torch`, `transformers`, and the editable `hypencoder-paper` install requirement for running our sample app. ## Contents | 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 | Each directory holds `passage_encoder.onnx`, `query_encoder.onnx` and `tokenizer.json`. The four passage encoders are **not** interchangeable — each checkpoint is a separate fine-tune, so the document towers differ. Pair a passage encoder only with the query encoder from the same directory. ## I/O contract Identical across depths except for the number of query-encoder outputs. **`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]`. Inputs are float32 rather than int64 because Vespa tensors have no int64 cell type; the export wrapper casts internally. Weight matrices are pre-transposed to `(out, in)` so they arrive in Vespa's alphabetical dimension order. ## Scoring Each q-net block is `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: ```python import numpy as np def layer_norm(v, eps=1e-5): return (v - v.mean(-1, keepdims=True)) / np.sqrt(v.var(-1, keepdims=True) + eps) def score_2layer(x0, W0, b0, W1, b1, Wout): relu0 = np.maximum(x0 @ W0.T + b0, 0.0) res0 = layer_norm(relu0) + x0 # residual on the first block relu1 = np.maximum(res0 @ W1.T + b1, 0.0) return layer_norm(relu1) @ Wout # no residual before the final projection ``` For the deeper checkpoints, treat [`q_net.py`](https://github.com/jfkback/hypencoder-paper/blob/main/hypencoder_cb/modeling/q_net.py) as authoritative for where residuals and layer norms are applied — the snippet above is written for the 2-block case only. ## Usage with onnxruntime ```python import numpy as np, onnxruntime as ort from tokenizers import Tokenizer D = "2_layer" tok = Tokenizer.from_file(f"{D}/tokenizer.json") enc = ort.InferenceSession(f"{D}/passage_encoder.onnx") qenc = ort.InferenceSession(f"{D}/query_encoder.onnx") d = tok.encode("Mount Everest is Earth's highest mountain, at 8,849 metres.") doc_vec = enc.run(None, {"input_ids": np.array([d.ids], dtype=np.int64), "attention_mask": np.array([d.attention_mask], dtype=np.int64)} )[0][:, 0] # CLS pooling q = tok.encode("tallest mountain in the world") names = [o.name for o in qenc.get_outputs()] w = dict(zip(names, qenc.run(None, { "input_ids": np.array([q.ids], dtype=np.float32), # float, not int64 "attention_mask": np.array([q.attention_mask], dtype=np.float32)}))) print(score_2layer(doc_vec.astype(np.float64), w["W0"], w["b0"], w["W1"], w["b1"], w["Wout"])) ``` ## Usage with Vespa The passage encoder and tokenizer can be fetched **by URL at deploy time**, so they never enter your application package: ```xml cls false ``` The query encoder **must be a local file** — `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: ```sh mkdir -p app/models curl -L -o app/models/query_encoder.onnx \ https://huggingface.co/andreer/hypencoder-onnx/resolve/main/2_layer/query_encoder.onnx ``` ``` onnx-model query_encoder { file: models/query_encoder.onnx input "input_ids": query(input_ids) input "attention_mask": query(attention_mask) } ``` **The sample app's rank profile implements 2 blocks only.** Using `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. ## Modifications relative to the base models Per Apache-2.0 §4(b): no weights were retrained or altered numerically. The export wrapper (a) returns the q-net weight tensors directly instead of a callable, (b) casts float `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. ## Citation ```bibtex @inproceedings{killingback2025hypencoder, title = {Hypencoder: Hypernetworks for Information Retrieval}, author = {Killingback, Julian and Zeng, Hansi and Zamani, Hamed}, booktitle = {SIGIR}, year = {2025} } ``` Weights © the original authors, Apache-2.0. This repository redistributes them in ONNX form under the same licence.