Lean4-helper / scripts /build_leandojo_index.py
p4r5kpftnp-cmd
Swap MiniLM retriever for LeanDojo's ByT5 premise encoder
d05440d
Raw
History Blame Contribute Delete
3.91 kB
#!/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()