Run full model inference on each token string. Returns (N, D) float32.
(model, tokenizer, tokens: list, device: str,
batch_size: int = 64,
checkpoint_path: str = None)
| 169 | # ── Extraction ─────────────────────────────────────────────────────── |
| 170 | |
| 171 | def extract_embeddings(model, tokenizer, tokens: list, device: str, |
| 172 | batch_size: int = 64, |
| 173 | checkpoint_path: str = None) -> np.ndarray: |
| 174 | """Run full model inference on each token string. Returns (N, D) float32.""" |
| 175 | |
| 176 | # Check for checkpoint |
| 177 | start_idx = 0 |
| 178 | all_vecs = [] |
| 179 | if checkpoint_path and os.path.exists(checkpoint_path): |
| 180 | data = np.load(checkpoint_path) |
| 181 | all_vecs = list(data["vectors"]) |
| 182 | start_idx = len(all_vecs) |
| 183 | print(f" resuming from checkpoint: {start_idx}/{len(tokens)} tokens") |
| 184 | |
| 185 | model.eval() |
| 186 | total = len(tokens) |
| 187 | t0 = time.time() |
| 188 | |
| 189 | with torch.no_grad(): |
| 190 | for batch_start in range(start_idx, total, batch_size): |
| 191 | batch_end = min(batch_start + batch_size, total) |
| 192 | batch_tokens = tokens[batch_start:batch_end] |
| 193 | |
| 194 | # nomic-embed-code requires search_query or search_document prefix |
| 195 | # For single tokens, we use the token as-is (query mode) |
| 196 | texts = [f"search_query: {t}" for t in batch_tokens] |
| 197 | |
| 198 | encoded = tokenizer( |
| 199 | texts, |
| 200 | padding=True, |
| 201 | truncation=True, |
| 202 | max_length=64, |
| 203 | return_tensors="pt" |
| 204 | ).to(device) |
| 205 | |
| 206 | outputs = model(**encoded) |
| 207 | |
| 208 | # Mean pooling over non-padding tokens |
| 209 | attention_mask = encoded["attention_mask"] |
| 210 | token_embeddings = outputs.last_hidden_state |
| 211 | input_mask_expanded = ( |
| 212 | attention_mask.unsqueeze(-1) |
| 213 | .expand(token_embeddings.size()) |
| 214 | .float() |
| 215 | ) |
| 216 | sum_embeddings = torch.sum( |
| 217 | token_embeddings * input_mask_expanded, dim=1 |
| 218 | ) |
| 219 | sum_mask = torch.clamp(input_mask_expanded.sum(dim=1), min=1e-9) |
| 220 | mean_pooled = sum_embeddings / sum_mask |
| 221 | |
| 222 | # Truncate to OUTPUT_DIM if model outputs more (Matryoshka) |
| 223 | if mean_pooled.shape[1] > OUTPUT_DIM: |
| 224 | mean_pooled = mean_pooled[:, :OUTPUT_DIM] |
| 225 | |
| 226 | # L2 normalize |
| 227 | mean_pooled = torch.nn.functional.normalize(mean_pooled, p=2, dim=1) |
| 228 |