zero v1 — M7 lookup table, run p35w-2m-s2500
Browse files- README.md +171 -0
- config.json +40 -0
- model.npz +3 -0
- special_tokens_map.json +37 -0
- tokenizer.json +0 -0
- tokenizer_config.json +62 -0
- vocab.txt +0 -0
- zero_encoder.py +89 -0
README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language: en
|
| 4 |
+
library_name: numpy
|
| 5 |
+
tags:
|
| 6 |
+
- retrieval
|
| 7 |
+
- sentence-similarity
|
| 8 |
+
- asymmetric-dual-encoder
|
| 9 |
+
- edge
|
| 10 |
+
- quantized
|
| 11 |
+
base_model: NovaSearch/stella_en_400M_v5
|
| 12 |
+
pipeline_tag: feature-extraction
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# zero — a query encoder with no transformer in it
|
| 16 |
+
|
| 17 |
+
`zero` is the query side of an **asymmetric dual encoder**. Documents are indexed once, in the
|
| 18 |
+
cloud, by a large frozen encoder ([`NovaSearch/stella_en_400M_v5`](https://huggingface.co/NovaSearch/stella_en_400M_v5),
|
| 19 |
+
1024-d). Queries are encoded on the edge by **this table**: 30,522 × 1024 int8 rows and one
|
| 20 |
+
pooling rule. Encoding a query is a gather and a weighted sum — **no transformer, no matmul,
|
| 21 |
+
no GPU**. Sub-millisecond per query on one CPU core; the whole query asset is **31.8 MB**.
|
| 22 |
+
|
| 23 |
+
It was distilled from stella so that its output lands in stella's document space. Cosine
|
| 24 |
+
similarity against stella document vectors is the score.
|
| 25 |
+
|
| 26 |
+
> **Research preview.** This model **missed its own release bar** — see [Results](#results).
|
| 27 |
+
> It is published so the architecture can be tested, not as a recommended drop-in retriever.
|
| 28 |
+
> Read the results section before using it for anything.
|
| 29 |
+
|
| 30 |
+
## Usage
|
| 31 |
+
|
| 32 |
+
The query side needs `numpy` and `tokenizers`. That is the entire runtime.
|
| 33 |
+
|
| 34 |
+
```python
|
| 35 |
+
# pip install numpy tokenizers huggingface_hub
|
| 36 |
+
from huggingface_hub import snapshot_download
|
| 37 |
+
import sys
|
| 38 |
+
|
| 39 |
+
d = snapshot_download("DylanCouzon/zero-query-encoder-v1") # ~94 MB
|
| 40 |
+
sys.path.insert(0, d)
|
| 41 |
+
from zero_encoder import ZeroQueryEncoder
|
| 42 |
+
|
| 43 |
+
enc = ZeroQueryEncoder(d, variant="int8") # or "fp16"
|
| 44 |
+
q = enc.encode(["how do mrna vaccines work?"]) # (1, 1024), L2-normalized
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Documents are encoded by the frozen teacher — **pin the revision**, the table is only valid
|
| 48 |
+
against this exact document space:
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
# pip install sentence-transformers
|
| 52 |
+
from sentence_transformers import SentenceTransformer
|
| 53 |
+
import numpy as np
|
| 54 |
+
|
| 55 |
+
doc_model = SentenceTransformer(
|
| 56 |
+
"NovaSearch/stella_en_400M_v5",
|
| 57 |
+
revision="ffeb2b7ee715c226d4ffe5e4619f7dbb48624c20",
|
| 58 |
+
trust_remote_code=True,
|
| 59 |
+
# required unless xformers is installed; also the pinned setting the table was distilled under
|
| 60 |
+
config_kwargs={"use_memory_efficient_attention": False, "unpad_inputs": False},
|
| 61 |
+
)
|
| 62 |
+
docs = [
|
| 63 |
+
"mRNA vaccines deliver a strand of messenger RNA encoding a viral antigen.",
|
| 64 |
+
"The Treaty of Westphalia ended the Thirty Years' War in 1648.",
|
| 65 |
+
]
|
| 66 |
+
D = doc_model.encode(docs, normalize_embeddings=True) # no prefix on the document side
|
| 67 |
+
|
| 68 |
+
scores = q @ D.T
|
| 69 |
+
print(docs[int(np.argmax(scores))])
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
That asymmetry is the point: `doc_model` runs once per document, in the cloud. `enc` runs on
|
| 73 |
+
every query, on the device, and costs almost nothing.
|
| 74 |
+
|
| 75 |
+
### The rule, if you reimplement it
|
| 76 |
+
|
| 77 |
+
Tokenize with the bundled WordPiece tokenizer (`add_special_tokens=True`, truncate at 512,
|
| 78 |
+
**no padding**, no prefix). A token appearing `c` times in the query carries **total weight
|
| 79 |
+
`sqrt(c)`**, not `c`. Sum the rows, divide by the weight sum, L2-normalize. An empty or
|
| 80 |
+
near-zero-norm bag falls back to the normalized `[CLS]` row (id **101**; row 0 is `[PAD]`).
|
| 81 |
+
`zero_encoder.py` is 80 lines and is the reference. `config.json` carries the rule and its
|
| 82 |
+
fingerprint (`adb24fb2e8cad66f`).
|
| 83 |
+
|
| 84 |
+
## Files
|
| 85 |
+
|
| 86 |
+
| file | what |
|
| 87 |
+
|---|---|
|
| 88 |
+
| `model.npz` | `rows_int8` (30522×1024) + `int8_scale`, and `rows_fp16` for reference |
|
| 89 |
+
| `config.json` | the frozen preprocessing rule, teacher pin, document-encoder spec, shas |
|
| 90 |
+
| `zero_encoder.py` | the whole query path — numpy + tokenizers, no torch |
|
| 91 |
+
| `tokenizer.json`, `vocab.txt`, … | stella's WordPiece tokenizer, copied at the pinned revision |
|
| 92 |
+
|
| 93 |
+
Per-token learned weights are **folded into the rows**, so the int8 artifact is self-contained.
|
| 94 |
+
`int8` is the variant every published number below was measured on; it was measured
|
| 95 |
+
quality-free against `fp16` (upper bound **0.00013** nDCG@10).
|
| 96 |
+
|
| 97 |
+
## Results
|
| 98 |
+
|
| 99 |
+
nDCG@10, exact search (no ANN, so recall is not a confound), on six BEIR datasets. These are
|
| 100 |
+
**confirmatory** numbers from a single pre-registered run against frozen comparator vectors —
|
| 101 |
+
the system was frozen (sha `a7007b1a…`) before any of them were observed.
|
| 102 |
+
|
| 103 |
+
| system | arguana | fiqa | nfcorpus | scidocs | scifact | trec-covid | **avg-6** |
|
| 104 |
+
|---|---|---|---|---|---|---|---|
|
| 105 |
+
| **`zero` (int8)** | 0.5916 | 0.3728 | 0.3124 | 0.1677 | 0.6101 | 0.5490 | **0.4339** |
|
| 106 |
+
| `zero` + BM25, fused | 0.5975 | 0.4026 | 0.3497 | 0.1881 | 0.7068 | 0.7018 | **0.4911** |
|
| 107 |
+
| BM25 alone | 0.4878 | 0.2532 | 0.3180 | 0.1565 | 0.6791 | 0.6099 | 0.4174 |
|
| 108 |
+
| stella, symmetric (the teacher ceiling) | 0.6369 | 0.5536 | 0.4134 | 0.2395 | 0.7796 | 0.8234 | 0.5744 |
|
| 109 |
+
|
| 110 |
+
The three registered comparisons, paired bootstrap + sign-flip, Holm-corrected across the family:
|
| 111 |
+
|
| 112 |
+
| | Δ | 95% CI | verdict |
|
| 113 |
+
|---|---|---|---|
|
| 114 |
+
| `zero` > LightRetriever dense (0.4583) | **−0.0243** | [−0.0405, −0.0086] | **miss, resolved below the bar** |
|
| 115 |
+
| `zero` > BM25 (0.4174) | +0.0165 | [+0.0017, +0.0311] | CI passes, multiplicity does not — unresolved |
|
| 116 |
+
| fused > OpenSearch sparse (0.4868) | +0.0043 | [−0.0063, +0.0151] | **statistical tie** |
|
| 117 |
+
|
| 118 |
+
**What that means.** A pure lookup table retains **75.5%** of its teacher's quality on these six
|
| 119 |
+
sets at ~1/1000 of the query-side cost, and beats BM25 on average — but it does **not** beat a
|
| 120 |
+
comparable small dense query encoder, and the honest headline is the miss. The one bright spot
|
| 121 |
+
is fusion: `zero` + BM25 is a **statistical tie with OpenSearch's learned sparse retriever**
|
| 122 |
+
while its query side remains a table lookup plus token counts.
|
| 123 |
+
|
| 124 |
+
### Caveats you should read before trusting a number
|
| 125 |
+
|
| 126 |
+
- **Teacher contamination.** stella discloses **ArguAna** and **FiQA** in its training data —
|
| 127 |
+
exactly this system's two strongest datasets. On the four sets with no disclosed overlap
|
| 128 |
+
(nfcorpus, scidocs, scifact, trec-covid), `zero` is **below BM25** (−0.0311 [−0.0517, −0.0109]).
|
| 129 |
+
- **Distribution.** Training was Wikipedia- and e-commerce-shaped. Retention is 0.915 on
|
| 130 |
+
in-distribution development sets and 0.755 on these six. Expect the low number out of domain.
|
| 131 |
+
- **English only**, 512 wordpieces, WordPiece-30522 vocab. Out-of-vocabulary terms degrade to
|
| 132 |
+
their subword rows; heavily fragmented queries are where the gap with a real encoder is widest.
|
| 133 |
+
- The table is a **bag of tokens**. Word order, negation and syntax are not represented at all.
|
| 134 |
+
|
| 135 |
+
## Costs
|
| 136 |
+
|
| 137 |
+
| | |
|
| 138 |
+
|---|---|
|
| 139 |
+
| query asset (int8 rows + scales + tokenizer) | **31.8 MB** |
|
| 140 |
+
| query encode, batch 1, one CPU core | **0.38 ms** (`zero_encoder.py` measures ~0.07 ms) |
|
| 141 |
+
| hydration (cold load to first query) | **0.22 s** |
|
| 142 |
+
| document index, 1024-d fp16 | 2.05 GB per 1M documents |
|
| 143 |
+
| document index, 1024-d int8 | 1.02 GB per 1M documents |
|
| 144 |
+
|
| 145 |
+
For reference at the document side: LightRetriever 3.07, OpenSearch sparse 1.40,
|
| 146 |
+
bge-small 0.77 GB/1M. `zero`'s document index is not cheap — the trade is all on the query side.
|
| 147 |
+
|
| 148 |
+
## Training
|
| 149 |
+
|
| 150 |
+
L2 regression of the table's pooled output onto stella's query embeddings, over 340,850
|
| 151 |
+
approved pairs plus 220,632 query-text-only rows. Sources: **Amazon ESCI** (Apache-2.0),
|
| 152 |
+
**FEVER**, **HotpotQA**, **SQuAD**, **NQ-open**, **TriviaQA**, **Mr. TyDi (en)**.
|
| 153 |
+
|
| 154 |
+
**MS MARCO is permanently excluded** from this lineage — its terms forbid commercial use.
|
| 155 |
+
(Measured cost of that exclusion: +0.0058 [−0.0015, +0.0131] avg-6 had it been included, which
|
| 156 |
+
still misses the bar. The gap is architectural, not licensing.)
|
| 157 |
+
|
| 158 |
+
### Attribution
|
| 159 |
+
|
| 160 |
+
NQ, SQuAD, HotpotQA, FEVER and Mr. TyDi derive from Wikipedia and are **CC BY-SA** (3.0/4.0).
|
| 161 |
+
Amazon ESCI is Apache-2.0. The teacher, `NovaSearch/stella_en_400M_v5`, is MIT.
|
| 162 |
+
|
| 163 |
+
## Citation / provenance
|
| 164 |
+
|
| 165 |
+
```
|
| 166 |
+
run_id p35w-2m-s2500
|
| 167 |
+
table sha256 a7007b1a6af120b976f093fd69ddcb5001996ec0b84b5864b4fd25d7af878abf
|
| 168 |
+
teacher NovaSearch/stella_en_400M_v5 @ ffeb2b7ee715c226d4ffe5e4619f7dbb48624c20
|
| 169 |
+
preproc prefix="" · add_special_tokens · max_length=512 · pool_mode=sqrt
|
| 170 |
+
preproc fingerprint adb24fb2e8cad66f
|
| 171 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dim": 1024,
|
| 3 |
+
"document_encoder": {
|
| 4 |
+
"cls_id": 101,
|
| 5 |
+
"config_kwargs": {
|
| 6 |
+
"unpad_inputs": false,
|
| 7 |
+
"use_memory_efficient_attention": false
|
| 8 |
+
},
|
| 9 |
+
"dim": 1024,
|
| 10 |
+
"doc_prefix": "",
|
| 11 |
+
"max_length": 512,
|
| 12 |
+
"name": "stella-400M-v5",
|
| 13 |
+
"pooling": "mean",
|
| 14 |
+
"post_dense": "2_Dense_1024",
|
| 15 |
+
"query_prefix": "Instruct: Given a web search query, retrieve relevant passages that answer the query.\nQuery: ",
|
| 16 |
+
"repo": "NovaSearch/stella_en_400M_v5",
|
| 17 |
+
"revision": "ffeb2b7ee715c226d4ffe5e4619f7dbb48624c20",
|
| 18 |
+
"tokenizer_id": "bert-wordpiece-30522",
|
| 19 |
+
"vocab": 30522
|
| 20 |
+
},
|
| 21 |
+
"fallback_token_id": 101,
|
| 22 |
+
"learned_weights": false,
|
| 23 |
+
"model_type": "zero-lookup-table",
|
| 24 |
+
"preproc": {
|
| 25 |
+
"add_special_tokens": true,
|
| 26 |
+
"max_length": 512,
|
| 27 |
+
"pool_mode": "sqrt",
|
| 28 |
+
"prefix": ""
|
| 29 |
+
},
|
| 30 |
+
"preproc_fingerprint": "adb24fb2e8cad66f",
|
| 31 |
+
"recommended_variant": "int8",
|
| 32 |
+
"run_id": "p35w-2m-s2500",
|
| 33 |
+
"similarity": "cosine",
|
| 34 |
+
"source_table_sha256": "a7007b1a6af120b976f093fd69ddcb5001996ec0b84b5864b4fd25d7af878abf",
|
| 35 |
+
"teacher": "NovaSearch/stella_en_400M_v5",
|
| 36 |
+
"teacher_revision": "ffeb2b7ee715c226d4ffe5e4619f7dbb48624c20",
|
| 37 |
+
"version": "v1",
|
| 38 |
+
"vocab": 30522,
|
| 39 |
+
"weights_folded": true
|
| 40 |
+
}
|
model.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a7007b1a6af120b976f093fd69ddcb5001996ec0b84b5864b4fd25d7af878abf
|
| 3 |
+
size 93886950
|
special_tokens_map.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cls_token": {
|
| 3 |
+
"content": "[CLS]",
|
| 4 |
+
"lstrip": false,
|
| 5 |
+
"normalized": false,
|
| 6 |
+
"rstrip": false,
|
| 7 |
+
"single_word": false
|
| 8 |
+
},
|
| 9 |
+
"mask_token": {
|
| 10 |
+
"content": "[MASK]",
|
| 11 |
+
"lstrip": false,
|
| 12 |
+
"normalized": false,
|
| 13 |
+
"rstrip": false,
|
| 14 |
+
"single_word": false
|
| 15 |
+
},
|
| 16 |
+
"pad_token": {
|
| 17 |
+
"content": "[PAD]",
|
| 18 |
+
"lstrip": false,
|
| 19 |
+
"normalized": false,
|
| 20 |
+
"rstrip": false,
|
| 21 |
+
"single_word": false
|
| 22 |
+
},
|
| 23 |
+
"sep_token": {
|
| 24 |
+
"content": "[SEP]",
|
| 25 |
+
"lstrip": false,
|
| 26 |
+
"normalized": false,
|
| 27 |
+
"rstrip": false,
|
| 28 |
+
"single_word": false
|
| 29 |
+
},
|
| 30 |
+
"unk_token": {
|
| 31 |
+
"content": "[UNK]",
|
| 32 |
+
"lstrip": false,
|
| 33 |
+
"normalized": false,
|
| 34 |
+
"rstrip": false,
|
| 35 |
+
"single_word": false
|
| 36 |
+
}
|
| 37 |
+
}
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"added_tokens_decoder": {
|
| 3 |
+
"0": {
|
| 4 |
+
"content": "[PAD]",
|
| 5 |
+
"lstrip": false,
|
| 6 |
+
"normalized": false,
|
| 7 |
+
"rstrip": false,
|
| 8 |
+
"single_word": false,
|
| 9 |
+
"special": true
|
| 10 |
+
},
|
| 11 |
+
"100": {
|
| 12 |
+
"content": "[UNK]",
|
| 13 |
+
"lstrip": false,
|
| 14 |
+
"normalized": false,
|
| 15 |
+
"rstrip": false,
|
| 16 |
+
"single_word": false,
|
| 17 |
+
"special": true
|
| 18 |
+
},
|
| 19 |
+
"101": {
|
| 20 |
+
"content": "[CLS]",
|
| 21 |
+
"lstrip": false,
|
| 22 |
+
"normalized": false,
|
| 23 |
+
"rstrip": false,
|
| 24 |
+
"single_word": false,
|
| 25 |
+
"special": true
|
| 26 |
+
},
|
| 27 |
+
"102": {
|
| 28 |
+
"content": "[SEP]",
|
| 29 |
+
"lstrip": false,
|
| 30 |
+
"normalized": false,
|
| 31 |
+
"rstrip": false,
|
| 32 |
+
"single_word": false,
|
| 33 |
+
"special": true
|
| 34 |
+
},
|
| 35 |
+
"103": {
|
| 36 |
+
"content": "[MASK]",
|
| 37 |
+
"lstrip": false,
|
| 38 |
+
"normalized": false,
|
| 39 |
+
"rstrip": false,
|
| 40 |
+
"single_word": false,
|
| 41 |
+
"special": true
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"clean_up_tokenization_spaces": true,
|
| 45 |
+
"cls_token": "[CLS]",
|
| 46 |
+
"do_lower_case": true,
|
| 47 |
+
"mask_token": "[MASK]",
|
| 48 |
+
"max_length": 8000,
|
| 49 |
+
"model_max_length": 32768,
|
| 50 |
+
"pad_to_multiple_of": null,
|
| 51 |
+
"pad_token": "[PAD]",
|
| 52 |
+
"pad_token_type_id": 0,
|
| 53 |
+
"padding_side": "right",
|
| 54 |
+
"sep_token": "[SEP]",
|
| 55 |
+
"stride": 0,
|
| 56 |
+
"strip_accents": null,
|
| 57 |
+
"tokenize_chinese_chars": true,
|
| 58 |
+
"tokenizer_class": "BertTokenizer",
|
| 59 |
+
"truncation_side": "right",
|
| 60 |
+
"truncation_strategy": "longest_first",
|
| 61 |
+
"unk_token": "[UNK]"
|
| 62 |
+
}
|
vocab.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
zero_encoder.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standalone query encoder for the `zero` lookup table. numpy + tokenizers, no torch.
|
| 2 |
+
|
| 3 |
+
This is the whole query path. It is a vocab x dim table of vectors: tokenize the query,
|
| 4 |
+
gather one row per token, take a count-saturated weighted mean, L2 normalize. There is no
|
| 5 |
+
transformer and no matrix multiply -- encoding a query is a gather and a sum.
|
| 6 |
+
|
| 7 |
+
The output lives in the document space of the frozen teacher (NovaSearch/stella_en_400M_v5,
|
| 8 |
+
revision pinned in config.json), so it is only meaningful against document vectors produced
|
| 9 |
+
by that exact encoder. Cosine similarity is the score.
|
| 10 |
+
|
| 11 |
+
Conformance: this file reproduces the frozen training-time query path (m7src/table.py
|
| 12 |
+
`encode_pooled`) to < 1e-5 max-abs on the release fixtures; see m11/release/test_conformance.py.
|
| 13 |
+
"""
|
| 14 |
+
import json
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
from tokenizers import Tokenizer
|
| 19 |
+
|
| 20 |
+
EPS = 1e-6
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ZeroQueryEncoder:
|
| 24 |
+
"""The released query encoder.
|
| 25 |
+
|
| 26 |
+
variant: "int8" is the artifact the published numbers were measured on (31 MB);
|
| 27 |
+
"fp16" is the same table before quantization (62 MB), included for reference --
|
| 28 |
+
int8 was measured quality-free against it (upper bound 0.00013 nDCG@10).
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, model_dir, variant="int8"):
|
| 32 |
+
d = Path(model_dir)
|
| 33 |
+
self.config = json.loads((d / "config.json").read_text())
|
| 34 |
+
pre = self.config["preproc"]
|
| 35 |
+
if pre["pool_mode"] != "sqrt" or pre["prefix"] != "" or not pre["add_special_tokens"]:
|
| 36 |
+
raise ValueError(f"this file implements the frozen M7 rule only, got {pre}")
|
| 37 |
+
self.max_length = int(pre["max_length"])
|
| 38 |
+
self.fallback_id = int(self.config["fallback_token_id"])
|
| 39 |
+
|
| 40 |
+
z = np.load(d / "model.npz")
|
| 41 |
+
if variant == "int8":
|
| 42 |
+
self.rows = z["rows_int8"].astype(np.float32) * z["int8_scale"][:, None]
|
| 43 |
+
elif variant == "fp16":
|
| 44 |
+
self.rows = z["rows_fp16"].astype(np.float32)
|
| 45 |
+
else:
|
| 46 |
+
raise ValueError(f"variant must be 'int8' or 'fp16', got {variant!r}")
|
| 47 |
+
self.variant = variant
|
| 48 |
+
|
| 49 |
+
self.tokenizer = Tokenizer.from_file(str(d / "tokenizer.json"))
|
| 50 |
+
self.tokenizer.enable_truncation(max_length=self.max_length)
|
| 51 |
+
# stella's tokenizer.json ships with padding-to-512 enabled. Padding would put ~500
|
| 52 |
+
# [PAD] rows into every bag; the frozen path (transformers, padding off) never sees one.
|
| 53 |
+
self.tokenizer.no_padding()
|
| 54 |
+
self._fallback = self._normalize(self.rows[self.fallback_id])
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def dim(self):
|
| 58 |
+
return self.rows.shape[1]
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def _normalize(v):
|
| 62 |
+
n = float(np.linalg.norm(v))
|
| 63 |
+
if n <= EPS: # degenerate row: fall back to e_0
|
| 64 |
+
e0 = np.zeros_like(v)
|
| 65 |
+
e0[0] = 1.0
|
| 66 |
+
return e0
|
| 67 |
+
return v / n
|
| 68 |
+
|
| 69 |
+
def encode(self, texts):
|
| 70 |
+
"""texts: str or list[str] -> float32 array (n, dim), L2-normalized."""
|
| 71 |
+
if isinstance(texts, str):
|
| 72 |
+
texts = [texts]
|
| 73 |
+
out = np.empty((len(texts), self.dim), dtype=np.float32)
|
| 74 |
+
for i, enc in enumerate(self.tokenizer.encode_batch(texts)):
|
| 75 |
+
out[i] = self._encode_ids(enc.ids)
|
| 76 |
+
return out
|
| 77 |
+
|
| 78 |
+
def _encode_ids(self, ids):
|
| 79 |
+
if not ids:
|
| 80 |
+
return self._fallback
|
| 81 |
+
uniq, counts = np.unique(np.asarray(ids, dtype=np.int64), return_counts=True)
|
| 82 |
+
# count saturation: a token seen c times carries TOTAL weight sqrt(c), not c. The
|
| 83 |
+
# denominator cancels under the final L2 normalize; it is kept so the intermediate
|
| 84 |
+
# stays in the released rule's range and the degeneracy threshold means the same thing.
|
| 85 |
+
w = np.sqrt(counts, dtype=np.float32)
|
| 86 |
+
vec = (self.rows[uniq] * w[:, None]).sum(0) / max(float(w.sum()), EPS)
|
| 87 |
+
if float(np.linalg.norm(vec)) <= EPS:
|
| 88 |
+
return self._fallback
|
| 89 |
+
return self._normalize(vec).astype(np.float32)
|