Spaces:
Sleeping
Sleeping
| 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 |