| """Llama Nemotron Embed VL 1B v2: dense chunk/query embeddings (parsed approach). |
| |
| Bi-encoder, 2048-dim, mean-pooled. We embed text only in v1 (section chunks |
| and MiniCPM figure/table descriptions); the model can also embed page/figure |
| images into the same space — a future upgrade path. |
| |
| Its remote code supports transformers 4.56+, so it shares an environment with |
| MiniCPM (and the Space). The model is a module-level global for the same |
| ZeroGPU reason as the others: module-level CUDA tensors are shared with the |
| GPU worker, function arguments are pickled and trust_remote_code classes are |
| not picklable. |
| |
| embed_texts runs at ingest (Modal); embed_query runs inside the ask pipeline's |
| single @spaces.GPU call. Outputs are L2-normalized, so cosine == dot product. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
| import torch |
| from transformers import AutoModel |
|
|
| from core.constants import ( |
| EMBED_TEXT_BATCH_SIZE, |
| EMBED_TEXT_MAX_LENGTH, |
| NEMOTRON_EMBED_MODEL_ID, |
| NEMOTRON_EMBED_REVISION, |
| ) |
| from core.vram import log_vram |
|
|
| _MODEL = ( |
| AutoModel.from_pretrained( |
| NEMOTRON_EMBED_MODEL_ID, |
| revision=NEMOTRON_EMBED_REVISION, |
| trust_remote_code=True, |
| dtype=torch.bfloat16, |
| attn_implementation="sdpa", |
| ) |
| .to("cuda") |
| .eval() |
| ) |
| |
| _MODEL.processor.p_max_length = EMBED_TEXT_MAX_LENGTH |
| log_vram("load-nemotron-embed") |
|
|
|
|
| def _normalize(emb: torch.Tensor) -> torch.Tensor: |
| return torch.nn.functional.normalize(emb.float(), p=2, dim=-1) |
|
|
|
|
| def embed_texts(texts: list[str]) -> np.ndarray: |
| """Embed chunk texts -> [n, dim] float16, L2-normalized. Must run on GPU.""" |
| out = [] |
| with torch.inference_mode(): |
| for i in range(0, len(texts), EMBED_TEXT_BATCH_SIZE): |
| emb = _MODEL.encode_documents(texts=texts[i : i + EMBED_TEXT_BATCH_SIZE]) |
| out.append(_normalize(emb).to(torch.float16).cpu().numpy()) |
| return np.concatenate(out, axis=0) |
|
|
|
|
| def embed_query(question: str) -> np.ndarray: |
| """Embed one query -> [dim] float32, L2-normalized. Must run on GPU |
| (called from within a @spaces.GPU context on the Space).""" |
| with torch.inference_mode(): |
| emb = _MODEL.encode_queries([question]) |
| return _normalize(emb)[0].cpu().numpy() |
|
|
|
|
| class NemotronEmbed: |
| MODEL_ID = NEMOTRON_EMBED_MODEL_ID |
|
|
| def embed_texts(self, texts: list[str]) -> np.ndarray: |
| return embed_texts(texts) |
|
|
| def embed_query(self, question: str) -> np.ndarray: |
| return embed_query(question) |
|
|