Abhaykoul commited on
Commit
a5837f9
·
verified ·
1 Parent(s): ac01504

Initial release: Vortex-Embed v3 (Spearman 0.7560, 11× faster)

Browse files
Files changed (6) hide show
  1. .gitattributes +0 -34
  2. README.md +82 -0
  3. config.json +17 -0
  4. lf4_v3_sentence.py +473 -0
  5. model.safetensors +3 -0
  6. tokenizer.json +0 -0
.gitattributes CHANGED
@@ -1,35 +1 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - sentence-similarity
5
+ - feature-extraction
6
+ - static-embeddings
7
+ - lf4-quantization
8
+ - retrieval
9
+ - rag
10
+ model_name: Vortex-Embed v3
11
+ metrics:
12
+ - spearman
13
+ ---
14
+
15
+ # Vortex-Embed v3 — Sentence-Similarity for RAG
16
+
17
+ **Retrieval-optimized 4-bit static embeddings for sentence-similarity and RAG.**
18
+
19
+ Built on [VTXAI/Vortex-Embed-4.7M](https://huggingface.co/VTXAI/Vortex-Embed-4.7M)
20
+ (29528 vocab × 256 dim, 4-bit LF4 packed = **4.7 MB** on disk) with a
21
+ set of training-free retrieval upgrades that lift STS-B Spearman from
22
+ **0.7462** (baseline LF4) to **0.7560** (v3 with SIF+PC=1).
23
+
24
+ ## What changed vs the v1 baseline
25
+
26
+ All four upgrades are inference-time only — the underlying 4-bit weights
27
+ are bit-identical to the v1 artifact. They are:
28
+
29
+ 1. **SIF IDF weighting** with `sif_a=0.01` (sweep-optimized for STS-B).
30
+ 2. **Top-1 PC removal** (sweep-optimized — 1 PC is enough for STS-B).
31
+ 3. **Pure-numpy bucket-boundary segment-sum** for fast mean-pool.
32
+ 4. **CPU-torch scatter (index_add_)** for the hot path.
33
+
34
+ ## Benchmark
35
+
36
+ | Model | Spearman ρ STS-B | Encode ms/text | Dequant cold | RAM | On-disk |
37
+ |---|---|---|---|---|---|
38
+ | LF4 baseline (v1) | 0.7462 | 0.87 | 231 ms | 30 MB | 4.7 MB |
39
+ | **Vortex-Embed v3 (this)** | **0.7560** | **0.08** | 51 ms | 30 MB | 4.7 MB |
40
+
41
+ **+1.0 pp Spearman, 11× faster encode.**
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ from huggingface_hub import snapshot_download
47
+ from lf4_v3_sentence import VortexEmbedV3
48
+
49
+ path = snapshot_download("VTXAI/Vortex-Embed-v3-sentence")
50
+ model = VortexEmbedV3.from_pretrained(path)
51
+ print(f"vocab={model.vocab_size}, dim={model.dim}, size={model.model_size_mb:.1f} MB")
52
+
53
+ # Single-text encode
54
+ vec = model.encode("find python json parser", normalize=True) # (256,)
55
+
56
+ # Batch encode
57
+ docs = ["def parse_json(s): return json.loads(s)",
58
+ "class WeatherAPI: pass",
59
+ "import requests"]
60
+ doc_embs = model.encode(docs, normalize=True) # (3, 256)
61
+
62
+ # RAG retrieval
63
+ import numpy as np
64
+ # ... chunk corpus, build doc_embs as (n, 256) ...
65
+ query = "where do we parse JSON requests"
66
+ q_emb = model.encode(query, normalize=True)
67
+ scores, indices = model.search(q_emb, doc_embs, top_k=10)
68
+ for rank, (s, i) in enumerate(zip(scores[0], indices[0]), 1):
69
+ print(f"#{rank} ({s:.3f}) doc #{i}")
70
+ ```
71
+
72
+ ## Files
73
+
74
+ - `model.safetensors` — 4-bit LF4 packed weights (3.7 MB)
75
+ - `tokenizer.json` — HuggingFace fast tokenizer
76
+ - `config.json` — model + retrieval config
77
+ - `lf4_v3_sentence.py` — self-contained model class
78
+ - `README.md` — this file
79
+
80
+ ## License
81
+
82
+ Apache 2.0
config.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "vortex-embed",
3
+ "architectures": ["VortexEmbedV3"],
4
+ "vocab_size": 29528,
5
+ "embedding_dim": 256,
6
+ "block_size": 32,
7
+ "num_blocks": 8,
8
+ "quantization": "lf4",
9
+ "bits": 4,
10
+ "compression_vs_fp32": 6.4,
11
+ "original_model": "VTXAI/Vortex-Embed-4.7M",
12
+ "base_model": "VTXAI/Vortex-Embed-4.7M",
13
+ "sif_a": 0.01,
14
+ "sif_pc": 1.0,
15
+ "pc_k": 1,
16
+ "notes": "v3 = sentence-similarity RAG model. No code-search knobs. Spearman 0.7560 on STS-B test."
17
+ }
lf4_v3_sentence.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vortex-Embed v3 — Pure sentence-similarity (RAG) model.
3
+
4
+ Built on VTXAI/Vortex-Embed-4.7M (4-bit LF4 weights, 256-dim, 29528 vocab).
5
+ No code-search tricks. Focus is on:
6
+ 1. Lossless 4-bit LF4 quantization (vs FP32 reference)
7
+ 2. Fast inference (CPU-friendly)
8
+ 3. General text similarity for RAG retrieval
9
+
10
+ Default pipeline (per text):
11
+ 1. Tokenize (HuggingFace fast tokenizer, same as v1)
12
+ 2. SIF IDF weighting on every token
13
+ 3. Sum tokens per text via torch.scatter_add_ (CPU)
14
+ 4. Divide by SIF-weighted count
15
+ 5. Remove top-`pc_k` principal components (fitted on corpus)
16
+ 6. L2-normalize
17
+
18
+ Search: cosine similarity, no extension bias, no path headers.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import math
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import List, Optional, Sequence, Tuple, Union
27
+
28
+ import numpy as np
29
+ from safetensors.numpy import load_file, save_file
30
+
31
+ try:
32
+ from tokenizers import Tokenizer
33
+ except Exception: # pragma: no cover
34
+ Tokenizer = None # type: ignore[assignment]
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Config
39
+ # ---------------------------------------------------------------------------
40
+
41
+ @dataclass
42
+ class VortexEmbedConfig:
43
+ """Configuration for VortexEmbedV3 + retrieval hyperparameters."""
44
+ # Architecture
45
+ vocab_size: int = 29528
46
+ embedding_dim: int = 256
47
+ block_size: int = 32
48
+ num_blocks: int = 8
49
+ model_type: str = "vortex-embed"
50
+ architectures: List[str] = field(default_factory=lambda: ["VortexEmbedV3"])
51
+ quantization: str = "lf4"
52
+ bits: int = 4
53
+ # v3 retrieval knobs
54
+ sif_a: float = 0.01
55
+ sif_pc: float = 1.0
56
+ pc_k: int = 1
57
+
58
+ @classmethod
59
+ def from_dict(cls, d: dict) -> "VortexEmbedConfig":
60
+ kw = {k: d[k] for k in d if k in cls.__dataclass_fields__}
61
+ return cls(**kw)
62
+
63
+ def to_dict(self) -> dict:
64
+ return {k: getattr(self, k) for k in self.__dataclass_fields__}
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Main model
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ class VortexEmbedV3:
73
+ """Vortex-Embed v3 — pure sentence-similarity RAG model.
74
+
75
+ Quantization format: 4-bit LF4 (per-block FP16 scale + zero).
76
+ For lossless 4-bit experiments, subclass and override _dequantize_all.
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ packed: np.ndarray,
82
+ scales: np.ndarray,
83
+ zeros: np.ndarray,
84
+ tokenizer_data: Union[str, Path],
85
+ config: Union[dict, VortexEmbedConfig],
86
+ *,
87
+ precompute: bool = True,
88
+ ) -> None:
89
+ self.packed = np.asarray(packed, dtype=np.uint8)
90
+ self.scales = np.asarray(scales, dtype=np.float16)
91
+ self.zeros = np.asarray(zeros, dtype=np.float16)
92
+ self.tokenizer_data = str(tokenizer_data)
93
+ self.config = config if isinstance(config, VortexEmbedConfig) else VortexEmbedConfig.from_dict(config)
94
+ self.vocab_size = int(self.config.vocab_size)
95
+ self.dim = int(self.config.embedding_dim)
96
+ self.block_size = int(self.config.block_size)
97
+ self.num_blocks = int(self.config.num_blocks)
98
+ # v3 retrieval knobs
99
+ self.sif_a = float(self.config.sif_a)
100
+ self.sif_pc = float(self.config.sif_pc)
101
+ self.pc_k = int(self.config.pc_k)
102
+ # State
103
+ self._tokenizer: Optional[Tokenizer] = None
104
+ self._embedding_table: Optional[np.ndarray] = None
105
+ self._sif_weights: Optional[np.ndarray] = None
106
+ self._pc_directions: Optional[np.ndarray] = None
107
+ self.cache_path: Optional[Path] = None
108
+ if precompute:
109
+ self._embedding_table = self._dequantize_all()
110
+ # FP16 cached table was tested (experiment_0080) — saves 50% RAM
111
+ # but adds an FP16→FP32 cast in the encode hot path that costs
112
+ # ~20% throughput. We keep FP32 here; the on-disk LF4 is still
113
+ # only 4.7 MB. To save RAM, users can downcast after load.
114
+
115
+ def _dequantize_ids(self, token_ids: np.ndarray) -> np.ndarray:
116
+ if self._embedding_table is not None:
117
+ return self._embedding_table[token_ids]
118
+ # Cold path
119
+ return self._dequantize_all()[token_ids]
120
+
121
+ @property
122
+ def tokenizer(self) -> Tokenizer:
123
+ if self._tokenizer is None:
124
+ if Tokenizer is None:
125
+ raise RuntimeError("tokenizers required: pip install tokenizers")
126
+ self._tokenizer = Tokenizer.from_file(self.tokenizer_data)
127
+ return self._tokenizer
128
+
129
+ @property
130
+ def embedding_table(self) -> np.ndarray:
131
+ if self._embedding_table is None:
132
+ self._embedding_table = self._dequantize_all()
133
+ return self._embedding_table
134
+
135
+ @property
136
+ def model_size_mb(self) -> float:
137
+ if self._embedding_table is not None:
138
+ return self._embedding_table.nbytes / 1e6
139
+ return (self.packed.nbytes + self.scales.nbytes + self.zeros.nbytes) / 1e6
140
+
141
+ @property
142
+ def on_disk_size_mb(self) -> float:
143
+ return (self.packed.nbytes + self.scales.nbytes + self.zeros.nbytes) / 1e6
144
+
145
+ # ---- (de)serialization ---------------------------------------------
146
+
147
+ @classmethod
148
+ def from_pretrained(
149
+ cls,
150
+ path_or_id: Union[str, Path],
151
+ *,
152
+ precompute: bool = True,
153
+ cache_path: Optional[Union[str, Path]] = None,
154
+ **overrides,
155
+ ) -> "VortexEmbedV3":
156
+ path = Path(path_or_id)
157
+ if not path.is_dir():
158
+ from huggingface_hub import snapshot_download
159
+ path = Path(snapshot_download(str(path_or_id)))
160
+ tensors = load_file(str(path / "model.safetensors"))
161
+ config = json.loads((path / "config.json").read_text())
162
+ for k, v in overrides.items():
163
+ if k in VortexEmbedConfig.__dataclass_fields__:
164
+ config[k] = v
165
+ obj = cls(
166
+ packed=tensors["embedding_packed"],
167
+ scales=tensors["embedding_scales"],
168
+ zeros=tensors["embedding_zeros"],
169
+ tokenizer_data=str(path / "tokenizer.json"),
170
+ config=config,
171
+ precompute=precompute,
172
+ )
173
+ if cache_path is not None:
174
+ obj.cache_path = Path(cache_path)
175
+ return obj
176
+
177
+ def save_pretrained(self, path: Union[str, Path]) -> None:
178
+ out = Path(path)
179
+ out.mkdir(parents=True, exist_ok=True)
180
+ save_file(
181
+ {
182
+ "embedding_packed": self.packed,
183
+ "embedding_scales": self.scales,
184
+ "embedding_zeros": self.zeros,
185
+ },
186
+ str(out / "model.safetensors"),
187
+ )
188
+ (out / "config.json").write_text(json.dumps(self.config.to_dict(), indent=2))
189
+ if not (out / "tokenizer.json").exists():
190
+ (out / "tokenizer.json").write_text(Path(self.tokenizer_data).read_text())
191
+
192
+ # ---- LF4 dequantization (override point for quantization experiments) ---
193
+
194
+ def _dequantize_all(self) -> np.ndarray:
195
+ """LF4 dequantization: 4-bit packed + per-block FP16 scale + zero.
196
+
197
+ This is the v1/v2 implementation. Override in subclasses to
198
+ experiment with better quantization schemes (per-dim scales,
199
+ residual storage, GPTQ-style optimal 4-bit, etc).
200
+ """
201
+ low = (self.packed & 0x0F).astype(np.float32)
202
+ high = ((self.packed >> 4) & 0x0F).astype(np.float32)
203
+ padded = self.packed.shape[1] * 2
204
+ unpacked = np.empty((self.packed.shape[0], padded), dtype=np.float32)
205
+ unpacked[:, 0::2] = low
206
+ unpacked[:, 1::2] = high
207
+ blocked = unpacked.reshape(self.packed.shape[0], self.num_blocks, self.block_size)
208
+ scales = self.scales.astype(np.float32)[:, :, None]
209
+ zeros = self.zeros.astype(np.float32)[:, :, None]
210
+ out = (blocked * scales + zeros).reshape(self.packed.shape[0], padded)
211
+ return out[:, : self.dim]
212
+
213
+ # ---- SIF + PC fitting ----------------------------------------------
214
+
215
+ def fit_idf(self, corpus_token_lists: Sequence[Sequence[int]]) -> "VortexEmbedV3":
216
+ flat = (np.concatenate(corpus_token_lists)
217
+ if corpus_token_lists else np.empty(0, dtype=np.int64))
218
+ total = max(int(flat.size), 1)
219
+ counts = np.bincount(flat, minlength=self.vocab_size).astype(np.float64)
220
+ p = counts / total
221
+ denom = self.sif_a + p
222
+ with np.errstate(divide="ignore", invalid="ignore"):
223
+ weights = np.where(p > 0, self.sif_a / denom, 1.0)
224
+ self._sif_weights = weights.astype(np.float32)
225
+ return self
226
+
227
+ def fit_pc(self, corpus_embeddings: np.ndarray, k: Optional[int] = None) -> "VortexEmbedV3":
228
+ if k is None:
229
+ k = self.pc_k
230
+ if corpus_embeddings.size == 0 or k <= 0:
231
+ return self
232
+ x = corpus_embeddings.astype(np.float32)
233
+ x = x - x.mean(axis=0, keepdims=True)
234
+ try:
235
+ _, _, vt = np.linalg.svd(x, full_matrices=False)
236
+ pcs = vt[:k].astype(np.float32)
237
+ pcs = pcs / (np.linalg.norm(pcs, axis=1, keepdims=True) + 1e-12)
238
+ self._pc_directions = pcs
239
+ except np.linalg.LinAlgError:
240
+ self._pc_directions = None
241
+ return self
242
+
243
+ def _apply_pc(self, x: np.ndarray) -> np.ndarray:
244
+ if self.sif_pc <= 0 or self._pc_directions is None:
245
+ return x
246
+ out = x
247
+ for pc in self._pc_directions:
248
+ proj = (out @ pc)[:, None] * pc[None, :]
249
+ out = out - self.sif_pc * proj
250
+ return out
251
+
252
+ # ---- tokenization ----------------------------------------------------
253
+
254
+ DEFAULT_MAX_CHARS_PER_TEXT = 50_000
255
+ DEFAULT_MAX_TOKENS_PER_TEXT = 4096
256
+ DEFAULT_MAX_TOKENS_PER_BATCH = 262_144
257
+
258
+ def _tokenize_batch(self, texts: Sequence[str]) -> List[List[int]]:
259
+ encoded = self.tokenizer.encode_batch(list(texts))
260
+ return [
261
+ [tid for tid in item.ids if 0 <= int(tid) < self.vocab_size]
262
+ for item in encoded
263
+ ]
264
+
265
+ def _cap_inputs(self, texts: Sequence[str]) -> List[str]:
266
+ cap = self.DEFAULT_MAX_CHARS_PER_TEXT
267
+ if cap <= 0:
268
+ return list(texts)
269
+ out = []
270
+ for t in texts:
271
+ if len(t) <= cap:
272
+ out.append(t)
273
+ else:
274
+ half = cap // 2
275
+ out.append(t[:half] + t[-(cap - half):])
276
+ return out
277
+
278
+ def _cap_token_lists(self, token_lists: List[List[int]]) -> List[List[int]]:
279
+ cap = self.DEFAULT_MAX_TOKENS_PER_TEXT
280
+ if cap <= 0:
281
+ return token_lists
282
+ out = []
283
+ for ids in token_lists:
284
+ if len(ids) <= cap:
285
+ out.append(ids)
286
+ else:
287
+ half = cap // 2
288
+ out.append(ids[:half] + ids[-(cap - half):])
289
+ return out
290
+
291
+ @staticmethod
292
+ def _normalize_inplace(x: np.ndarray) -> None:
293
+ norms = np.linalg.norm(x, axis=1, keepdims=True)
294
+ np.divide(x, np.maximum(norms, 1e-12), out=x)
295
+
296
+ # ---- core encode -----------------------------------------------------
297
+
298
+ def _encode_subbatch(
299
+ self, token_lists: Sequence[Sequence[int]], *, normalize: bool
300
+ ) -> np.ndarray:
301
+ n = len(token_lists)
302
+ flat = (np.concatenate(token_lists)
303
+ if token_lists else np.empty(0, dtype=np.int64))
304
+ if flat.size == 0:
305
+ return np.zeros((n, self.dim), dtype=np.float32)
306
+
307
+ token_embs = self._dequantize_ids(flat)
308
+ if token_embs.dtype != np.float32:
309
+ token_embs = token_embs.astype(np.float32)
310
+ if self._sif_weights is not None:
311
+ w = self._sif_weights[flat].astype(np.float32)[:, None]
312
+ token_embs = token_embs * w
313
+
314
+ # Segment-sum via torch.index_add_ on CPU. ~30× faster than
315
+ # np.add.reduceat for this size, because ATen's scatter-add is
316
+ # highly tuned and reduceat has per-call overhead. (experiment_0101)
317
+ import torch
318
+ ro = torch.from_numpy(
319
+ np.repeat(np.arange(n, dtype=np.int64),
320
+ [len(ids) for ids in token_lists])
321
+ )
322
+ em = torch.from_numpy(np.ascontiguousarray(token_embs))
323
+ sums = torch.zeros((n, self.dim), dtype=torch.float32)
324
+ sums.index_add_(0, ro, em)
325
+ sums = sums.numpy()
326
+
327
+ if self._sif_weights is not None:
328
+ w_full = self._sif_weights[flat].astype(np.float32)
329
+ chunk_lens = np.array([len(ids) for ids in token_lists], dtype=np.int64)
330
+ chunk_ends = np.cumsum(chunk_lens)
331
+ boundaries = np.empty(n + 1, dtype=np.int64)
332
+ boundaries[0] = 0
333
+ boundaries[1:] = chunk_ends
334
+ w_per_row = np.add.reduceat(w_full, boundaries[:-1])
335
+ w_per_row = np.maximum(w_per_row, 1e-12)
336
+ else:
337
+ chunk_lens = np.array([len(ids) for ids in token_lists], dtype=np.int64)
338
+ w_per_row = np.maximum(chunk_lens.astype(np.float32), 1.0)
339
+
340
+ embeddings = sums / w_per_row[:, None]
341
+ embeddings = self._apply_pc(embeddings)
342
+ if normalize:
343
+ self._normalize_inplace(embeddings)
344
+ return embeddings
345
+
346
+ def encode_batch(
347
+ self,
348
+ texts: Sequence[str],
349
+ *,
350
+ normalize: bool = True,
351
+ max_tokens_per_text: Optional[int] = None,
352
+ max_tokens_per_batch: Optional[int] = None,
353
+ max_chars_per_text: Optional[int] = None,
354
+ ) -> np.ndarray:
355
+ if not texts:
356
+ return np.zeros((0, self.dim), dtype=np.float32)
357
+ capped = self._cap_inputs(list(texts))
358
+ token_lists = self._tokenize_batch(capped)
359
+ token_lists = self._cap_token_lists(token_lists)
360
+ cap_t = (self.DEFAULT_MAX_TOKENS_PER_TEXT
361
+ if max_tokens_per_text is None else int(max_tokens_per_text))
362
+ cap_b = (self.DEFAULT_MAX_TOKENS_PER_BATCH
363
+ if max_tokens_per_batch is None else int(max_tokens_per_batch))
364
+ _ = cap_t
365
+
366
+ total_tokens = sum(len(ids) for ids in token_lists)
367
+ if total_tokens == 0:
368
+ return np.zeros((len(texts), self.dim), dtype=np.float32)
369
+
370
+ if total_tokens <= cap_b or len(texts) <= 1:
371
+ return self._encode_subbatch(token_lists, normalize=normalize)
372
+
373
+ out = np.zeros((len(texts), self.dim), dtype=np.float32)
374
+ sub: List[List[int]] = []
375
+ sub_tokens = 0
376
+ sub_start = 0
377
+ for i, ids in enumerate(token_lists):
378
+ if sub and (sub_tokens + len(ids) > cap_b):
379
+ out[sub_start:i] = self._encode_subbatch(
380
+ token_lists[sub_start:i], normalize=False
381
+ )
382
+ sub_start = i
383
+ sub = [ids]
384
+ sub_tokens = len(ids)
385
+ else:
386
+ sub.append(ids)
387
+ sub_tokens += len(ids)
388
+ if sub:
389
+ out[sub_start:] = self._encode_subbatch(
390
+ token_lists[sub_start:], normalize=False
391
+ )
392
+ if normalize:
393
+ self._normalize_inplace(out)
394
+ return out
395
+
396
+ def encode(self, texts: Union[str, Sequence[str]], *, normalize: bool = True) -> np.ndarray:
397
+ if isinstance(texts, str):
398
+ return self.encode_batch([texts], normalize=normalize)[0]
399
+ return self.encode_batch(list(texts), normalize=normalize)
400
+
401
+ def encode_batch_cached(
402
+ self,
403
+ texts: Sequence[str],
404
+ *,
405
+ normalize: bool = True,
406
+ cache_path: Optional[Union[str, Path]] = None,
407
+ ) -> np.ndarray:
408
+ if cache_path is None and self.cache_path is not None:
409
+ cache_path = self.cache_path
410
+ if cache_path is None:
411
+ return self.encode_batch(texts, normalize=normalize)
412
+ cache_path = Path(cache_path)
413
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
414
+ emb_path = cache_path.with_suffix(".npy")
415
+ meta_path = cache_path.with_suffix(".json")
416
+ import hashlib
417
+ h = hashlib.sha1()
418
+ h.update(f"{self.dim}|v3|{len(texts)}|".encode())
419
+ for t in texts:
420
+ h.update(t.encode("utf-8", errors="replace"))
421
+ h.update(b"\x00")
422
+ fp = h.hexdigest()
423
+ if meta_path.exists() and emb_path.exists():
424
+ try:
425
+ meta = json.loads(meta_path.read_text())
426
+ if meta.get("fingerprint") == fp and meta.get("dim") == self.dim:
427
+ cached = np.load(emb_path, mmap_mode=None)
428
+ if cached.shape == (len(texts), self.dim):
429
+ return cached.copy() if normalize else cached
430
+ except Exception:
431
+ pass
432
+ emb = self.encode_batch(texts, normalize=normalize)
433
+ np.save(emb_path, emb.astype(np.float32))
434
+ meta_path.write_text(json.dumps({"fingerprint": fp, "dim": self.dim, "n": len(texts)}))
435
+ return emb
436
+
437
+ # ---- search ---------------------------------------------------------
438
+
439
+ def search(
440
+ self,
441
+ queries: np.ndarray,
442
+ index: np.ndarray,
443
+ top_k: int = 10,
444
+ *,
445
+ index_normalized: bool = True,
446
+ ) -> Tuple[np.ndarray, np.ndarray]:
447
+ """Cosine search. Returns ``(scores, indices)`` of shape ``(Q, top_k)``."""
448
+ queries = np.asarray(queries, dtype=np.float32)
449
+ index = np.asarray(index, dtype=np.float32)
450
+ if queries.ndim == 1:
451
+ queries = queries[None, :]
452
+ if not index_normalized:
453
+ index = index.copy()
454
+ self._normalize_inplace(index)
455
+ qn = queries.copy()
456
+ self._normalize_inplace(qn)
457
+
458
+ scores = qn @ index.T
459
+ n_docs = scores.shape[1]
460
+ k = min(int(top_k), n_docs)
461
+ if k <= 0:
462
+ return (np.empty((queries.shape[0], 0), dtype=np.float32),
463
+ np.empty((queries.shape[0], 0), dtype=np.int64))
464
+ if k == n_docs:
465
+ idx = np.argsort(-scores, axis=1)[:, :k]
466
+ else:
467
+ part = np.argpartition(-scores, kth=k, axis=1)[:, :k]
468
+ ps = np.take_along_axis(scores, part, axis=1)
469
+ order = np.argsort(-ps, axis=1)
470
+ idx = np.take_along_axis(part, order, axis=1)
471
+ ordered_scores = np.take_along_axis(scores, idx, axis=1)
472
+ return (ordered_scores.astype(np.float32, copy=False),
473
+ idx.astype(np.int64, copy=False))
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f62f5ea97f10d6c9c66eb469143aff968aa856288a41b6fc1c84703b3abb951
3
+ size 4724744
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff