Visual Document Retrieval
Safetensors
sentence-transformers
colpali-engine
qwen3_5
vision-language
colbert
late-interaction
multi-vector
matryoshka
vidore
token-compression
Instructions to use tencent/EVIE-4.5B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use tencent/EVIE-4.5B with sentence-transformers:
from sentence_transformers import MultiVectorEncoder model = MultiVectorEncoder("tencent/EVIE-4.5B") queries = ["Which planet is known as the Red Planet?"] documents = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", ] query_embeddings = model.encode_query(queries) document_embeddings = model.encode_document(documents) similarities = model.similarity(query_embeddings, document_embeddings) print(similarities) - Notebooks
- Google Colab
- Kaggle
Download code/compress/eval_index.py from tencent/EVIE-4.5B: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/tencent/EVIE-4.5B/resolve/main/code/compress/eval_index.py
- Command line
-
hf download hf://tencent/EVIE-4.5B/code/compress/eval_index.py
-
curl -L -o eval_index.py https://huggingface.co/tencent/EVIE-4.5B/resolve/main/code/compress/eval_index.py
14.6 kB
| #!/usr/bin/env python3 | |
| """Independent MaxSim on a dump (raw) or HAC index. | |
| Does not re-encode. Reports NDCG / Recall / MAP / MRR at @1/@5/@10. | |
| Search timing is query-already-encoded MaxSim only — not product QPS. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from collections import defaultdict | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "shared")) | |
| import torch | |
| from common import ( | |
| BUDGET, | |
| CEILING, | |
| DUMP_PROTOCOL, | |
| HEAD_DIM, | |
| MVT, | |
| REQUIRED_KS, | |
| REQUIRED_METRICS, | |
| SOUP_NAME, | |
| WEIGHT, | |
| atomic_write_json, | |
| dump_ok, | |
| gib_per_million_pages, | |
| has_complete_metrics, | |
| load_qrels, | |
| padded_from_pack, | |
| w_tag, | |
| ) | |
| import eval as evie_eval # noqa: E402 | |
| from paths import forbid_venv_path # noqa: E402 | |
| DS_LABEL = evie_eval.DS_LABEL | |
| METRIC_NAMES = evie_eval.METRIC_NAMES | |
| def load_task_list(dump_root: Path) -> list[dict]: | |
| tasks_dir = dump_root / "tasks" | |
| recs = [] | |
| for path in sorted(tasks_dir.glob("*.json")): | |
| if dump_ok(path, dump_root): | |
| recs.append(json.loads(path.read_text(encoding="utf-8"))) | |
| if not recs: | |
| raise SystemExit(f"no complete dump tasks under {tasks_dir}") | |
| return recs | |
| def load_queries(dump_root: Path, rec: dict) -> torch.Tensor: | |
| blob = torch.load(dump_root / rec["queries"], map_location="cpu", weights_only=False) | |
| return padded_from_pack(blob["packed"], blob["offsets"]) | |
| def load_raw_docs(dump_root: Path, rec: dict) -> torch.Tensor: | |
| blob = torch.load(dump_root / rec["docs"], map_location="cpu", weights_only=False) | |
| return padded_from_pack(blob["packed"], blob["offsets"]) | |
| def load_clustered_docs(cluster_root: Path, rec: dict, tag: str, budget: int) -> tuple[torch.Tensor, dict]: | |
| path = Path(cluster_root) / tag / "docs" / f"{rec['corpus_id']}.pt" | |
| blob = torch.load(path, map_location="cpu", weights_only=False) | |
| padded = blob["padded"] | |
| if padded.ndim != 3 or int(padded.shape[-1]) != HEAD_DIM: | |
| raise RuntimeError(f"bad clustered tensor {path}: {tuple(padded.shape)}") | |
| if int(padded.shape[1]) != int(budget): | |
| raise RuntimeError(f"clustered K {padded.shape[1]} != budget {budget} at {path}") | |
| return padded, blob.get("stats") or {} | |
| def score_task(q_emb, d_emb, relevant, graded, device, chunk_q, chunk_d): | |
| q = q_emb.to(device) | |
| t0 = time.perf_counter() | |
| scores = evie_eval.chunked_maxsim(q, d_emb, chunk_q=chunk_q, chunk_d=chunk_d) | |
| dt = time.perf_counter() - t0 | |
| metrics = evie_eval.metrics_from_scores(scores, relevant, graded, list(REQUIRED_KS)) | |
| n_q = int(q_emb.shape[0]) | |
| return metrics, { | |
| "n_docs": int(d_emb.shape[0]), | |
| "n_queries": n_q, | |
| "doc_tokens": int(d_emb.shape[1]), | |
| "query_tokens_padded": int(q_emb.shape[1]), | |
| "search_sec": round(dt, 4), | |
| "search_ms_per_query": round(1000.0 * dt / max(n_q, 1), 4), | |
| "encode_ms_per_query": None, | |
| "note": "pure search; query vectors already dumped; not product QPS", | |
| "chunk_q": chunk_q, | |
| "chunk_d": chunk_d, | |
| "device": str(device), | |
| } | |
| def write_summary(out_dir: Path, recs: list[dict], expected: list[dict], args, mode: str, w): | |
| by_ds = defaultdict(list) | |
| failed = [r for r in recs if "error" in r] | |
| ok = [r for r in recs if "metrics" in r and has_complete_metrics(r["metrics"])] | |
| for rec in ok: | |
| by_ds[rec["dataset"]].append(rec) | |
| expected_by_ds = defaultdict(int) | |
| for rec in expected: | |
| expected_by_ds[rec["dataset"]] += 1 | |
| expected_tags = {rec["task"] for rec in expected} | |
| missing = sorted(expected_tags - {r["task"] for r in ok}) | |
| def avg_metric(rows, name): | |
| vals = [r["metrics"].get(name) for r in rows] | |
| vals = [v for v in vals if v is not None] | |
| return (sum(vals) / len(vals)) if vals else None | |
| ks = list(REQUIRED_KS) | |
| per_ds = {} | |
| for ds, rows in by_ds.items(): | |
| entry = {f"{m}@{k}": avg_metric(rows, f"{m}@{k}") for m in METRIC_NAMES for k in ks} | |
| entry["n_subsets"] = len(rows) | |
| entry["n_subsets_expected"] = expected_by_ds.get(ds, len(rows)) | |
| entry["n_queries"] = sum(r["info"]["n_queries"] for r in rows) | |
| entry["n_docs"] = sum(r["info"]["n_docs"] for r in rows) | |
| search = [r["info"]["search_ms_per_query"] for r in rows if r["info"].get("search_ms_per_query") is not None] | |
| entry["search_ms_per_query_mean_unweighted"] = ( | |
| sum(search) / len(search) if search else None | |
| ) | |
| per_ds[ds] = entry | |
| headline = { | |
| "V1 nDCG@10": per_ds.get("eval", {}).get("ndcg@10"), | |
| "V2 nDCG@10": per_ds.get("eval_v2", {}).get("ndcg@10"), | |
| "V3 nDCG@10": per_ds.get("eval_v3", {}).get("ndcg@10"), | |
| "JinaVDR nDCG@10": per_ds.get("JinaVDR", {}).get("ndcg@10"), | |
| } | |
| four = [headline[k] for k in ("V1 nDCG@10", "V2 nDCG@10", "V3 nDCG@10", "JinaVDR nDCG@10")] | |
| averages = { | |
| "avg4_ndcg@10": (sum(four) / 4 if all(v is not None for v in four) else None), | |
| } | |
| complete = not failed and not missing | |
| vec_per_page = 1.0 # overwritten below | |
| if mode == "k": | |
| vec_per_page = float(args.budget) | |
| storage_note = f"independent K{int(args.budget)} candidate layer, no context, no raw rerank" | |
| else: | |
| vec_per_page = None | |
| storage_note = "raw full sequence from dump; not a compressed index" | |
| summary = { | |
| "status": "complete" if complete else "partial", | |
| "run_name": args.run_name, | |
| "mode": mode, | |
| "position_weight": None if mode == "raw" else float(w), | |
| "budget": None if mode == "raw" else int(args.budget), | |
| "export_context": False, | |
| "head_dim": HEAD_DIM, | |
| "max_visual_tokens": MVT, | |
| "bidirectional_attention": "on", | |
| "weights": SOUP_NAME, | |
| "alpha": 0.5, | |
| "dump_protocol": DUMP_PROTOCOL, | |
| "eval_protocol": evie_eval.EVAL_PROTOCOL, | |
| "ks": ks, | |
| "expected_tasks": len(expected), | |
| "completed_tasks": len(ok), | |
| "n_failed": len(failed), | |
| "failed_tasks": failed, | |
| "missing_tasks": missing, | |
| "headline": headline, | |
| "averages": averages, | |
| "per_dataset": per_ds, | |
| "per_task": ok, | |
| "ceiling": CEILING, | |
| "storage": { | |
| "vectors_per_page": vec_per_page, | |
| "dim": HEAD_DIM, | |
| "bytes_per_vec": 2, | |
| "gib_per_million_pages": ( | |
| None if vec_per_page is None else round(gib_per_million_pages(vec_per_page), 4) | |
| ), | |
| "context": False, | |
| "two_stage": False, | |
| "note": storage_note, | |
| }, | |
| "search": { | |
| "kind": "independent_pure_search", | |
| "includes_query_encode": False, | |
| "not_product_qps": True, | |
| "two_stage": False, | |
| }, | |
| } | |
| atomic_write_json(out_dir / "summary.json", summary) | |
| lines = [ | |
| f"# {args.run_name} {mode}" | |
| + (f" K{int(args.budget)} {w_tag(w)}" if mode == "k" else ""), | |
| "", | |
| f"status `{summary['status']}` · {summary['completed_tasks']}/{summary['expected_tasks']}", | |
| "", | |
| "Independent retrieval. Queries are already dumped; ms below is MaxSim only, not product QPS.", | |
| "", | |
| "| board | nDCG@1 | nDCG@5 | nDCG@10 | Recall@1 | Recall@5 | Recall@10 | MAP@1 | MAP@5 | MAP@10 | MRR@1 | MRR@5 | MRR@10 |", | |
| "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |", | |
| ] | |
| def pct(v): | |
| return "—" if v is None else f"{v * 100:.2f}" | |
| for ds, label in DS_LABEL.items(): | |
| e = per_ds.get(ds) | |
| if not e: | |
| continue | |
| cells = " | ".join( | |
| pct(e.get(name)) | |
| for name in ( | |
| "ndcg@1", | |
| "ndcg@5", | |
| "ndcg@10", | |
| "recall@1", | |
| "recall@5", | |
| "recall@10", | |
| "map@1", | |
| "map@5", | |
| "map@10", | |
| "mrr@1", | |
| "mrr@5", | |
| "mrr@10", | |
| ) | |
| ) | |
| lines.append(f"| {label} | {cells} |") | |
| lines += [ | |
| "", | |
| f"uncompressed d{HEAD_DIM} eval V3 nDCG@10 = {CEILING['V3 nDCG@10']} " | |
| f"(source `{CEILING['source']}`)", | |
| "", | |
| ] | |
| (out_dir / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8") | |
| print( | |
| f"[eval] {summary['status']} {summary['completed_tasks']}/{summary['expected_tasks']} " | |
| f"V3 nDCG@10={headline.get('V3 nDCG@10')}", | |
| flush=True, | |
| ) | |
| if not complete: | |
| raise SystemExit(2) | |
| def parse_args(): | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument("--dump-dir", required=True) | |
| ap.add_argument("--output-dir", required=True) | |
| ap.add_argument("--cluster-root", default="") | |
| ap.add_argument("--mode", choices=("raw", "k"), required=True) | |
| ap.add_argument("--w", type=float, default=WEIGHT) | |
| ap.add_argument("--budget", type=int, default=BUDGET) | |
| ap.add_argument("--run-name", default="evie-compress") | |
| ap.add_argument("--chunk-q", type=int, default=64) | |
| ap.add_argument("--chunk-d", type=int, default=256) | |
| ap.add_argument("--resume", action="store_true") | |
| return ap.parse_args() | |
| def main(): | |
| args = parse_args() | |
| if args.mode == "k": | |
| if args.w is None: | |
| raise SystemExit("k mode requires --w") | |
| if not args.cluster_root: | |
| raise SystemExit("k mode requires --cluster-root") | |
| tag = w_tag(args.w) | |
| else: | |
| tag = "raw" | |
| args.w = None | |
| rank, world, local = evie_eval.setup_ddp() | |
| device = torch.device(f"cuda:{local}" if torch.cuda.is_available() else "cpu") | |
| dump_root = forbid_venv_path(args.dump_dir, "dump-dir") | |
| out_dir = forbid_venv_path(args.output_dir, "output-dir") | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| expected = load_task_list(dump_root) | |
| cached = {} | |
| if args.resume: | |
| cached = evie_eval.load_cached_task_map(out_dir, protocol=evie_eval.EVAL_PROTOCOL) | |
| cached = {k: v for k, v in cached.items() if has_complete_metrics(v.get("metrics"))} | |
| pending = [rec for rec in expected if rec["task"] not in cached] | |
| if world > 1: | |
| # greedy by query*doc; corpora are already on disk so V3 langs need not colocate | |
| pending = sorted(pending, key=lambda r: -(int(r["n_docs"]) * int(r["n_queries"]))) | |
| pending = pending[rank::world] | |
| print(f"[eval rank{rank}] {args.mode} {tag} pending {len(pending)}/{len(expected)} on {device}", flush=True) | |
| results = [] | |
| for rec in pending: | |
| tag_task = rec["task"] | |
| t0 = time.time() | |
| try: | |
| q_emb = load_queries(dump_root, rec) | |
| if args.mode == "raw": | |
| d_emb = load_raw_docs(dump_root, rec) | |
| else: | |
| d_emb, _stats = load_clustered_docs(Path(args.cluster_root), rec, tag, args.budget) | |
| relevant, graded = load_qrels(dump_root / rec["qrels"]) | |
| if len(relevant) != q_emb.shape[0]: | |
| raise RuntimeError( | |
| f"{tag_task}: qrels {len(relevant)} != queries {q_emb.shape[0]}" | |
| ) | |
| if d_emb.shape[0] != rec["n_docs"]: | |
| raise RuntimeError( | |
| f"{tag_task}: docs {d_emb.shape[0]} != dump n_docs {rec['n_docs']}" | |
| ) | |
| metrics, info = score_task( | |
| q_emb, d_emb, relevant, graded, device, args.chunk_q, args.chunk_d | |
| ) | |
| row = { | |
| "task": tag_task, | |
| "dataset": rec["dataset"], | |
| "subset": rec["subset"], | |
| "lang": rec.get("lang"), | |
| "fmt": rec["fmt"], | |
| "metrics": metrics, | |
| "info": info, | |
| "seconds": round(time.time() - t0, 1), | |
| "eval_protocol": evie_eval.EVAL_PROTOCOL, | |
| "dump_protocol": DUMP_PROTOCOL, | |
| "run_name": args.run_name, | |
| "mode": args.mode, | |
| "position_weight": args.w, | |
| } | |
| evie_eval.save_task_result(out_dir, row, run_name=args.run_name, contract_id=DUMP_PROTOCOL) | |
| results.append(row) | |
| print( | |
| f"[eval rank{rank}] {tag_task}: ndcg@10={metrics['ndcg@10']*100:.2f} " | |
| f"{info['n_queries']}q/{info['n_docs']}d search={info['search_ms_per_query']:.2f}ms/q", | |
| flush=True, | |
| ) | |
| except Exception as exc: | |
| import traceback | |
| traceback.print_exc() | |
| results.append({"task": tag_task, "dataset": rec["dataset"], "error": repr(exc)}) | |
| print(f"[eval rank{rank}] {tag_task} FAILED: {exc!r}", flush=True) | |
| atomic_write_json(out_dir / f"rank_{rank}.json", results) | |
| if world > 1: | |
| torch.distributed.destroy_process_group() | |
| if rank != 0: | |
| return | |
| deadline = time.monotonic() + int(os.environ.get("EVAL_FINALIZE_TIMEOUT_S", "21600")) | |
| missing_ranks = list(range(world)) | |
| last = 0.0 | |
| while missing_ranks: | |
| missing_ranks = [] | |
| for r in range(world): | |
| p = out_dir / f"rank_{r}.json" | |
| try: | |
| json.loads(p.read_text(encoding="utf-8")) | |
| except Exception: | |
| missing_ranks.append(r) | |
| if not missing_ranks: | |
| break | |
| now = time.monotonic() | |
| if now >= deadline: | |
| print(f"[eval] finalize timeout; missing {missing_ranks}", flush=True) | |
| break | |
| if now - last >= 60: | |
| print(f"[eval] waiting for ranks {missing_ranks}", flush=True) | |
| last = now | |
| time.sleep(2) | |
| merged = evie_eval.load_cached_task_map(out_dir, protocol=evie_eval.EVAL_PROTOCOL) | |
| rows = [] | |
| failed = [] | |
| for r in range(world): | |
| p = out_dir / f"rank_{r}.json" | |
| if not p.exists(): | |
| continue | |
| for rec in json.loads(p.read_text(encoding="utf-8")): | |
| if "error" in rec: | |
| failed.append(rec) | |
| elif rec.get("task") in merged: | |
| rows.append(merged[rec["task"]]) | |
| # durable cache wins | |
| rows = [merged[rec["task"]] for rec in expected if rec["task"] in merged] | |
| for miss in failed: | |
| if miss["task"] not in merged: | |
| rows.append(miss) | |
| write_summary(out_dir, rows, expected, args, args.mode, args.w) | |
| if __name__ == "__main__": | |
| main() | |