repair-guy / models /colembed.py
airayven7's picture
Sync from GitHub 16c4a03
8025053 verified
Raw
History Blame
3.72 kB
"""Nemotron ColEmbed v2: late-interaction page embeddings + MaxSim retrieval.
Two ZeroGPU entry points:
_embed_pages_on_gpu page images -> per-page token embeddings (index time)
_search_on_gpu question -> top-K (doc, page, score) via MaxSim over
batches of page embeddings streamed from the store
The model is a module-level global: ZeroGPU packs module-level CUDA tensors at
startup and shares them with the GPU worker, whereas function arguments are
pickled — and the trust_remote_code model class is not picklable.
forward_images/forward_queries return zero-padded [batch, tokens, dim] tensors
with real tokens L2-normalized, so padding rows are exactly zero. We strip them
before storing and rely on the same property when scoring zero-padded batches.
"""
from __future__ import annotations
import sys
import numpy as np
import spaces
import torch
from PIL import Image
from transformers import AutoModel
from core.constants import (
COLEMBED_ATTN,
COLEMBED_MODEL_ID,
EMBED_BATCH_SIZE,
EMBED_GPU_DURATION,
SCORE_PAGES_PER_BATCH,
SEARCH_GPU_DURATION,
)
_MODEL = (
AutoModel.from_pretrained(
COLEMBED_MODEL_ID,
trust_remote_code=True,
dtype=torch.bfloat16,
attn_implementation=COLEMBED_ATTN,
)
.to("cuda")
.eval()
)
# The remote code's forward_documents hardcodes DataLoader(num_workers=8), but
# the ZeroGPU worker is a daemonic process and may not spawn children
# ("daemonic processes are not allowed to have children"). Patch the DataLoader
# name in the model's own module to force in-process loading. A subclass (not a
# wrapper function) because the remote code also uses the name in isinstance().
_remote_module = sys.modules[type(_MODEL).__module__]
class _SingleProcessDataLoader(_remote_module.DataLoader):
def __init__(self, *args, **kwargs):
kwargs["num_workers"] = 0
super().__init__(*args, **kwargs)
_remote_module.DataLoader = _SingleProcessDataLoader
@spaces.GPU(duration=EMBED_GPU_DURATION)
def _embed_pages_on_gpu(images: list[Image.Image]) -> list[np.ndarray]:
with torch.no_grad():
embs = _MODEL.forward_images(images, batch_size=EMBED_BATCH_SIZE)
out = []
for emb in embs: # [tokens, dim]; zero rows are padding
mask = emb.abs().sum(dim=-1) > 0
out.append(emb[mask].to(torch.float16).cpu().numpy())
return out
@spaces.GPU(duration=SEARCH_GPU_DURATION)
def _search_on_gpu(question: str, store, doc_ids, top_k: int):
results = []
with torch.no_grad():
q = _MODEL.forward_queries([question], batch_size=1)[0].to(torch.float16)
for refs, batch in store.iter_page_batches(doc_ids, SCORE_PAGES_PER_BATCH):
emb = torch.from_numpy(batch).to(q.device) # [B, T, D] float16
sim = torch.einsum("qd,btd->bqt", q, emb).float()
scores = sim.amax(dim=2).sum(dim=1) # MaxSim: max over doc tokens, sum over query tokens
results.extend(
(doc_id, page, s)
for (doc_id, page), s in zip(refs, scores.tolist())
)
results.sort(key=lambda r: r[2], reverse=True)
return results[:top_k]
class ColEmbed:
MODEL_ID = COLEMBED_MODEL_ID
def embed_pages(self, images: list[Image.Image]) -> list[np.ndarray]:
"""Embed page images -> list of [n_tokens, dim] float16 arrays."""
return _embed_pages_on_gpu(images)
def search(
self, question: str, store, doc_ids: list[str] | None, top_k: int
) -> list[tuple[str, int, float]]:
"""Return the top_k (doc_id, page_num, score) across the given docs."""
return _search_on_gpu(question, store, doc_ids, top_k)