MajorTOM-Core-Viewer / helpers /TextSearch.py
caviri
Bake SigLIP weights into the image; trim search memory
f05241d
Raw
History Blame Contribute Delete
4.88 kB
import gc
import pyarrow as pa
import pyarrow.parquet as pq
import faiss
import torch
from open_clip import create_model_from_pretrained, get_tokenizer
# Columns actually used downstream. The metadata parquet also carries a
# 'geometry' column that is ~2GB uncompressed and is never read by this app,
# so it is deliberately left on disk.
METADATA_COLUMNS = ['grid_cell', 'file', 'row_idx']
class SearchSigLIP():
def __init__(self, index_path, metadata_path):
# Everything here is loaded lazily, on the first text search.
#
# Eagerly loading the faiss index (~1GB), the metadata table and the
# SigLIP weights (~3.5GB) costs more RAM than a cpu-basic Space has,
# and it happens at import time - so the container was being OOM-killed
# before it could serve the map, which does not need any of it.
self.index_path = index_path
self.metadata_path = metadata_path
self._ready = False
def _ensure_ready(self):
if self._ready:
return
print(f'Loading index from PATH={self.index_path}', flush=True)
self.init_index()
print('[DONE]', flush=True)
print(f'Loading metadata from PATH={self.metadata_path}', flush=True)
self.metadata = pq.read_table(self.metadata_path, columns=METADATA_COLUMNS)
print('[DONE]', flush=True)
self.init_model()
self._ready = True
def init_index(self):
self.cpu_index = faiss.read_index(self.index_path)
# Only move the index onto a GPU when one is actually usable. On CPU-only
# hardware faiss is built without the GPU symbols, so StandardGpuResources
# does not exist at all and we search the CPU index directly.
if hasattr(faiss, 'StandardGpuResources') and faiss.get_num_gpus() > 0:
res = faiss.StandardGpuResources()
cloner_options = faiss.GpuClonerOptions()
cloner_options.useFloat16LookupTables = True
self.index = faiss.index_cpu_to_gpu(res, 0, self.cpu_index, cloner_options)
else:
print('No GPU available for faiss - searching the CPU index.', flush=True)
self.index = self.cpu_index
self.index.nprobe = 32 # Higher = more accurate, slower
def init_model(self):
self.model, self.preprocess = create_model_from_pretrained('hf-hub:timm/ViT-SO400M-14-SigLIP-384')
self.model.eval()
self.tokenizer = get_tokenizer('hf-hub:timm/ViT-SO400M-14-SigLIP')
# Only encode_text is ever called here, so the vision tower is dead
# weight - roughly half the parameters. Dropping it frees over a GB.
# Guarded: if a future open_clip needs it, keeping the tower only costs
# memory, whereas a hard failure here would break search entirely.
try:
if hasattr(self.model, 'visual'):
del self.model.visual
gc.collect()
except Exception as e:
print(f'Could not release the vision tower: {e}', flush=True)
def encode_text(self, text, device=None):
if device is None:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
self.model.to(device)
with torch.no_grad():
text = self.tokenizer([text], context_length=self.model.context_length)
return self.model.encode_text(text.to(device))
def search_with_grid(self, query_vec, k=5):
# Prepare query
if isinstance(query_vec, torch.Tensor):
query_vec = query_vec.cpu().squeeze().numpy()
query_vec = query_vec.reshape(1, -1).astype('float32')
faiss.normalize_L2(query_vec)
# Search
distances, indices = self.index.search(query_vec, k)
# Flatten results
ids = indices[0]
scores = distances[0]
# We ignore -1 (which happens if k > total vectors, unlikely here)
valid_mask = ids != -1
valid_ids = ids[valid_mask]
valid_scores = scores[valid_mask]
if len(valid_ids) == 0:
return []
# Direct lookup by integer index, straight out of the Arrow table.
# Kept in Arrow rather than pandas: as an object-dtype DataFrame the
# 20M grid_cell strings alone cost well over a gigabyte.
matches = self.metadata.take(pa.array(valid_ids.astype('int64')))
results = matches.to_pylist()
for row, score in zip(results, valid_scores):
row['score'] = float(score)
return results
def faiss(self, text, k=1): # k - number of neighbours
# 0. Load the index/metadata/model if this is the first search
self._ensure_ready()
# 1. Compute query
q = self.encode_text(text)
# 2. Find Hits
results = self.search_with_grid(q, k=k)
return results