Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| Build a Lean-aware FAISS index from LeanDojo's pre-computed Mathlib premise | |
| embeddings. | |
| Downloads `kaiyuy/premise-embeddings-leandojo-lean4-retriever-byt5-small` | |
| (180,973 premises × 1472-dim ByT5 embeddings, ~1 GB) and compresses it into | |
| an IVFPQ FAISS index (~15 MB) wrapped in a LangChain vectorstore that uses | |
| ByT5 at query time. | |
| The IVFPQ compression keeps the committed index under GitHub's free LFS | |
| quota with ~95% recall vs flat search. | |
| Usage: | |
| python scripts/build_leandojo_index.py | |
| """ | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # faiss and torch both bring their own libomp.dylib on macOS and collide on | |
| # import. This env var is the documented workaround. | |
| os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE") | |
| import faiss | |
| import numpy as np | |
| from huggingface_hub import hf_hub_download | |
| from langchain_community.docstore.in_memory import InMemoryDocstore | |
| from langchain_community.vectorstores import FAISS as LCFAISS | |
| from langchain_core.documents import Document | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) | |
| EMBEDDINGS_REPO = "kaiyuy/premise-embeddings-leandojo-lean4-retriever-byt5-small" | |
| OUT_DIR = Path(__file__).resolve().parent.parent / "data" / "mathlib_index" | |
| def main(): | |
| print("Downloading LeanDojo premise embeddings (may be cached)…") | |
| emb_path = hf_hub_download(EMBEDDINGS_REPO, "embeddings.npy") | |
| dict_path = hf_hub_download(EMBEDDINGS_REPO, "dictionary.json") | |
| print("Loading embeddings…") | |
| embeddings = np.load(emb_path).astype(np.float32) | |
| print(f" {embeddings.shape} ({embeddings.nbytes / 1e6:.0f} MB float32)") | |
| # L2-normalize so cosine == inner product | |
| norms = np.linalg.norm(embeddings, axis=1, keepdims=True) | |
| embeddings = embeddings / np.maximum(norms, 1e-12) | |
| print("Loading dictionary…") | |
| with open(dict_path) as f: | |
| dict_entries = json.load(f) | |
| print(f" {len(dict_entries)} premises") | |
| print("Building langchain Documents…") | |
| docs = [] | |
| for i in range(len(embeddings)): | |
| entry = dict_entries[str(i)] | |
| docs.append( | |
| Document( | |
| page_content=entry["code"], | |
| metadata={ | |
| "name": entry["full_name"], | |
| "path": entry["path"], | |
| }, | |
| ) | |
| ) | |
| # IVFPQ — product-quantized inverted-file index. ~95% recall, ~60-100x smaller. | |
| d = embeddings.shape[1] # 1472 | |
| nlist = 512 # number of coarse clusters (~sqrt(N)) | |
| m = 64 # PQ segments (must divide d) | |
| nbits = 8 # bits per code | |
| print(f"Training IVFPQ index (d={d}, nlist={nlist}, m={m}, nbits={nbits})…") | |
| quantizer = faiss.IndexFlatIP(d) # inner product after L2 normalize = cosine | |
| index = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits, faiss.METRIC_INNER_PRODUCT) | |
| index.train(embeddings) | |
| index.add(embeddings) | |
| index.nprobe = 16 # search 16 clusters at query time | |
| # Free the raw embeddings before loading ByT5 (cuts peak RAM by ~1GB). | |
| del embeddings | |
| print("Wrapping into LangChain FAISS vectorstore…") | |
| # Import here so torch is loaded *after* FAISS training is done. | |
| from byt5_embedder import ByT5PremiseEmbedder # noqa: E402 | |
| embedder = ByT5PremiseEmbedder() | |
| docstore = InMemoryDocstore({str(i): docs[i] for i in range(len(docs))}) | |
| index_to_docstore_id = {i: str(i) for i in range(len(docs))} | |
| vectorstore = LCFAISS( | |
| embedding_function=embedder, | |
| index=index, | |
| docstore=docstore, | |
| index_to_docstore_id=index_to_docstore_id, | |
| ) | |
| print(f"Saving to {OUT_DIR}…") | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| vectorstore.save_local(str(OUT_DIR)) | |
| print("Output files:") | |
| for f in sorted(OUT_DIR.iterdir()): | |
| print(f" {f.name}: {f.stat().st_size / 1e6:.1f} MB") | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() | |