Spaces:
Running on Zero
Running on Zero
File size: 6,605 Bytes
86e797b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | """Official ColModernVBERT encoding, float16 disk cache, CPU MaxSim search."""
import hashlib
import json
from importlib.metadata import version
from pathlib import Path
from time import perf_counter
import torch
from PIL import Image
from torch.nn.utils.rnn import pad_sequence
from visual_doc_rag.data import write_json
from visual_doc_rag.maxsim import maxsim
from visual_doc_rag.models import cache_model
from visual_doc_rag.pages import load_pages
from visual_doc_rag.retrieve import rank_scores
class Encoder:
def __init__(self, data_dir: Path, model_info: dict, device: str):
from colpali_engine.models import ColModernVBert, ColModernVBertProcessor
self.device = torch.device(device)
self.processor = ColModernVBertProcessor.from_pretrained(
data_dir / "models" / model_info["adapter_path"],
local_files_only=True,
)
self.model = ColModernVBert.from_pretrained(
data_dir / "models" / model_info["base_path"],
local_files_only=True,
dtype=torch.float32,
attn_implementation="sdpa",
)
self.model.load_adapter(
str(data_dir / "models" / model_info["adapter_path"]),
adapter_kwargs={"local_files_only": True},
)
self.model.to(self.device).eval()
@torch.inference_mode()
def encode(self, *, image=None, query: str | None = None) -> torch.Tensor:
if (image is None) == (query is None):
raise ValueError("Provide exactly one image or query.")
inputs = (
self.processor.process_images([image])
if image is not None
else self.processor.process_texts([query])
).to(self.device)
embedding = self.model(**inputs)[0]
# Remove padding, preserving every genuine text/image/prompt token.
embedding = embedding[inputs["attention_mask"][0].bool()].detach().cpu()
if embedding.ndim != 2 or embedding.shape[0] == 0 or not torch.isfinite(embedding).all():
raise ValueError("Model returned an empty or nonfinite embedding.")
return embedding
def build_visual(
subset: str,
data_dir: Path,
source: dict,
device: str,
model_revision: str = "main",
base_revision: str | None = None,
) -> dict:
started = perf_counter()
model_info = cache_model(data_dir, model_revision, base_revision)
page_root, page_manifest = load_pages(subset, data_dir, source["revision"])
signature = {
"dataset_revision": source["revision"],
"model": model_info,
"encoder_dtype": "float32",
"storage_dtype": "float16",
"processor": "official defaults; process_texts without suffix; all unpadded tokens",
"attention": "sdpa",
"page_format": "original RGB PNG",
"library_versions": {
name: version(name) for name in ("colpali-engine", "transformers", "torch")
},
}
fingerprint = hashlib.sha256(json.dumps(signature, sort_keys=True).encode()).hexdigest()[:16]
root = data_dir / subset / "indexes"
embedding_root = root / "visual" / fingerprint
embedding_root.mkdir(parents=True, exist_ok=True)
if device == "cuda":
if not torch.cuda.is_available():
raise ValueError("CUDA unavailable. Run outside the GPU sandbox or use --device cpu.")
torch.cuda.reset_peak_memory_stats()
loading = perf_counter()
encoder = Encoder(data_dir, model_info, device)
load_seconds = perf_counter() - loading
computed = 0
pages = []
for index, page in enumerate(page_manifest["pages"]):
page_id = page["corpus_id"]
target = embedding_root / f"{page_id}.pt"
if target.exists():
embedding = torch.load(target, map_location="cpu", weights_only=True)
else:
with Image.open(page_root / page["file"]) as image:
embedding = encoder.encode(image=image).to(torch.float16)
temporary = target.with_suffix(".tmp")
torch.save(embedding, temporary)
temporary.replace(target)
computed += 1
if embedding.ndim != 2 or embedding.shape[1] != 128 or not torch.isfinite(embedding).all():
raise ValueError(f"Invalid cached embedding: {target}. Remove this file and retry.")
pages.append(
{
"corpus_id": page_id,
"file": str(target.relative_to(root)),
"tokens": embedding.shape[0],
}
)
if (index + 1) % 25 == 0 or index + 1 == len(page_manifest["pages"]):
print(f"Visual index: {index + 1}/{len(page_manifest['pages'])}", flush=True)
result = {
"dataset": source,
"signature": signature,
"pages": pages,
"build_seconds": perf_counter() - started,
"model_load_seconds": load_seconds,
"new_pages_encoded": computed,
"cached_pages_reused": len(pages) - computed,
"index_bytes": sum((root / page["file"]).stat().st_size for page in pages),
"peak_vram_bytes": torch.cuda.max_memory_allocated() if device == "cuda" else 0,
"device": device,
"device_name": torch.cuda.get_device_name() if device == "cuda" else "CPU",
}
write_json(root / "visual.json", result)
return result
class VisualIndex:
def __init__(self, root: Path, source: dict, batch_size: int = 32):
self.manifest = json.loads((root / "visual.json").read_text(encoding="utf-8"))
if self.manifest["dataset"]["revision"] != source["revision"]:
raise ValueError("Visual index dataset revision differs from downloaded data.")
self.page_ids = [page["corpus_id"] for page in self.manifest["pages"]]
self.batches = []
pages = self.manifest["pages"]
for start in range(0, len(pages), batch_size):
embeddings = [
torch.load(root / page["file"], map_location="cpu", weights_only=True).float()
for page in pages[start : start + batch_size]
]
padded = pad_sequence(embeddings, batch_first=True)
mask = (
torch.arange(padded.shape[1])[None, :]
< torch.tensor([embedding.shape[0] for embedding in embeddings])[:, None]
)
self.batches.append((padded, mask))
@torch.inference_mode()
def search(self, query: torch.Tensor) -> list[int]:
scores = torch.cat([maxsim(query, pages, mask) for pages, mask in self.batches])
return rank_scores(self.page_ids, scores.tolist())
|