DylanCouzon commited on
Commit
588581a
·
verified ·
1 Parent(s): e1423b2

zero v1 — M7 lookup table, run p35w-2m-s2500

Browse files
Files changed (8) hide show
  1. README.md +171 -0
  2. config.json +40 -0
  3. model.npz +3 -0
  4. special_tokens_map.json +37 -0
  5. tokenizer.json +0 -0
  6. tokenizer_config.json +62 -0
  7. vocab.txt +0 -0
  8. 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)