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/shared/data_loader.py from tencent/EVIE-4.5B: direct link, hf CLI and curl.
- Browser
- Download file 9.44 kB
-
https://huggingface.co/tencent/EVIE-4.5B/resolve/main/code/shared/data_loader.py
- Command line
-
hf download hf://tencent/EVIE-4.5B/code/shared/data_loader.py
-
curl -L -o data_loader.py https://huggingface.co/tencent/EVIE-4.5B/resolve/main/code/shared/data_loader.py
9.44 kB
| """Load `part_*.parquet` query/image pairs (and optional hard-negative shards).""" | |
| from __future__ import annotations | |
| import hashlib | |
| import os | |
| import time | |
| from collections import defaultdict | |
| from pathlib import Path | |
| from typing import Iterable | |
| import datasets | |
| import pyarrow as pa | |
| import pyarrow.compute as pc | |
| import pyarrow.parquet as pq | |
| from datasets import Dataset | |
| from datasets.arrow_writer import ArrowWriter | |
| from colpali_engine.data.dataset import ColPaliEngineDataset | |
| from paths import forbid_venv_path | |
| ALLOWED_SOURCES = ( | |
| "colpali_train_set", | |
| "VisRAG-Ret-Train-Synthetic-data", | |
| "VisRAG-Ret-Train-In-domain-data", | |
| "vdr-multilingual-train", | |
| "tatdqa_train", | |
| "tabfquad_train_set", | |
| ) | |
| _STREAM_BATCH_ROWS = 256 | |
| def _set_task_ids_from_sources( | |
| dataset: ColPaliEngineDataset, | |
| sources: pa.Array | pa.ChunkedArray, | |
| ) -> None: | |
| if isinstance(sources, pa.ChunkedArray): | |
| sources = sources.combine_chunks() | |
| encoded = pc.dictionary_encode(sources) | |
| dataset.task_ids = encoded.indices.to_numpy(zero_copy_only=False) | |
| dataset.task_names = encoded.dictionary.to_pylist() | |
| counts = pc.value_counts(encoded.indices).to_pylist() | |
| summary = { | |
| dataset.task_names[int(item["values"])]: int(item["counts"]) | |
| for item in counts | |
| } | |
| print(f"[data] task-consistent groups: {summary}") | |
| def _attach_task_ids( | |
| dataset: ColPaliEngineDataset, | |
| queries_ds: Dataset, | |
| corpus_ds: Dataset, | |
| docid_to_idx: dict[int, int] | None, | |
| ) -> None: | |
| if "source" not in corpus_ds.column_names: | |
| dataset.task_ids = [0] * len(dataset) | |
| dataset.task_names = ["all"] | |
| return | |
| positive_column = queries_ds.data.column("pos_target") | |
| if docid_to_idx is None: | |
| row_indices = positive_column | |
| else: | |
| row_indices = pa.array( | |
| [docid_to_idx[int(doc_id)] for doc_id in positive_column.to_pylist()], | |
| type=pa.int64(), | |
| ) | |
| sources = pc.take(corpus_ds.data.column("source"), row_indices) | |
| _set_task_ids_from_sources(dataset, sources) | |
| def _default_dataset_cache_dir() -> Path: | |
| override = os.environ.get("HF_DATASETS_CACHE") or os.environ.get("EVIE_DATASETS_CACHE") | |
| if override: | |
| return Path(override) | |
| return Path.home() / ".cache" / "huggingface" / "datasets" | |
| def _export_parts(data_root: Path) -> list[str]: | |
| parts = sorted(Path(data_root).glob("part_*.parquet")) | |
| if not parts: | |
| raise FileNotFoundError(f"No part_*.parquet under {data_root}") | |
| return [str(p) for p in parts] | |
| def _cache_dir() -> Path: | |
| d = Path(os.environ.get("HF_DATASETS_CACHE") or _default_dataset_cache_dir()) | |
| forbid_venv_path(d, "HF_DATASETS_CACHE") | |
| d.mkdir(parents=True, exist_ok=True) | |
| return d | |
| def _fingerprint(parts: list[str]) -> str: | |
| h = hashlib.sha1() | |
| for p in parts: | |
| st = Path(p).stat() | |
| h.update(Path(p).name.encode()) | |
| h.update(str(st.st_size).encode()) | |
| h.update(str(int(st.st_mtime)).encode()) | |
| return h.hexdigest()[:16] | |
| def _prepare_streamed_arrow(parts: list[str]) -> Path: | |
| """Rank 0 writes a shared Arrow cache; other ranks memory-map it. | |
| Avoid Dataset.filter here: concurrent DDP ranks would race on the same | |
| cache-*.arrow files. | |
| """ | |
| final = _cache_dir() / f"export_stream_{_fingerprint(parts)}.arrow" | |
| done = Path(str(final) + ".done") | |
| error = Path(str(final) + ".error") | |
| rank = int(os.environ.get("RANK", "0")) | |
| if done.exists() and final.exists(): | |
| return final | |
| if rank == 0: | |
| tmp = Path(str(final) + f".{os.getpid()}.tmp") | |
| error.unlink(missing_ok=True) | |
| try: | |
| writer = ArrowWriter(path=str(tmp)) | |
| total = 0 | |
| t0 = time.time() | |
| for i, p in enumerate(parts): | |
| for rb in pq.ParquetFile(p).iter_batches(batch_size=_STREAM_BATCH_ROWS): | |
| writer.write_table(pa.Table.from_batches([rb])) | |
| total += rb.num_rows | |
| print(f"[data] shard {i + 1}/{len(parts)}: {total:,} rows, {time.time() - t0:.0f}s", flush=True) | |
| writer.finalize() | |
| os.replace(tmp, final) | |
| done.touch() | |
| print(f"[data] arrow cache {final} ({total:,} rows)", flush=True) | |
| return final | |
| except Exception as exc: | |
| tmp.unlink(missing_ok=True) | |
| error.write_text(repr(exc), encoding="utf-8") | |
| raise | |
| waited = 0 | |
| timeout = int(os.environ.get("EVIE_DATA_CACHE_TIMEOUT", "7200")) | |
| while not (done.exists() and final.exists()): | |
| if error.exists(): | |
| raise RuntimeError(f"rank0 failed to build Arrow cache: {error.read_text()}") | |
| time.sleep(5) | |
| waited += 5 | |
| if waited >= timeout: | |
| raise TimeoutError(f"timed out waiting for Arrow cache after {timeout}s") | |
| return final | |
| def build_train_dataset( | |
| data_root: str | Path, | |
| sources: Iterable[str] = (), | |
| max_samples_per_source: int = 0, | |
| ) -> ColPaliEngineDataset: | |
| root = Path(data_root) | |
| parts = _export_parts(root) | |
| arrow = _prepare_streamed_arrow(parts) | |
| ds = Dataset.from_file(str(arrow)) | |
| selected = tuple(dict.fromkeys(sources)) | |
| if selected and set(selected) != set(ALLOWED_SOURCES): | |
| keep = set(selected) | |
| ds = ds.filter(lambda s: s in keep, input_columns="source") | |
| if max_samples_per_source: | |
| counts = defaultdict(int) | |
| indices = [] | |
| for i, source in enumerate(ds["source"]): | |
| if counts[source] < max_samples_per_source: | |
| indices.append(i) | |
| counts[source] += 1 | |
| ds = ds.select(indices) | |
| ds = ds.cast_column("image", datasets.Image()) | |
| tag = ",".join(selected) if selected else "all" | |
| print(f"[data] {len(ds):,} pairs from {root.name} ({tag})") | |
| wrapped = ColPaliEngineDataset(ds, pos_target_column_name="image") | |
| if "source" in ds.column_names: | |
| _set_task_ids_from_sources(wrapped, ds.data.column("source")) | |
| return wrapped | |
| def build_hardneg_dataset( | |
| hardneg_root: str | Path, | |
| data_root: str | Path, | |
| num_negatives: int = 2, | |
| max_samples: int = 0, | |
| use_negatives: bool = True, | |
| queries_subdir: str = "", | |
| ) -> ColPaliEngineDataset: | |
| from datasets import load_from_disk | |
| from colpali_engine.data.dataset import Corpus | |
| root = Path(hardneg_root) | |
| subdir = queries_subdir or os.environ.get("HARDNEG_SUBDIR", "judged") | |
| queries_ds = load_from_disk(str(root / subdir)) | |
| print(f"[data] hardneg subdir: {subdir}") | |
| corpus_path = root / "corpus" | |
| if corpus_path.is_dir(): | |
| corpus_ds = load_from_disk(str(corpus_path)) | |
| corpus_origin = str(corpus_path) | |
| else: | |
| parts = _export_parts(Path(data_root)) | |
| arrow = _prepare_streamed_arrow(parts) | |
| corpus_ds = Dataset.from_file(str(arrow)).cast_column("image", datasets.Image()) | |
| corpus_origin = f"export {Path(data_root).name}" | |
| if max_samples and len(queries_ds) > max_samples: | |
| queries_ds = queries_ds.shuffle(seed=42).select(range(max_samples)) | |
| docid_to_idx = None | |
| if "doc_id" in corpus_ds.column_names: | |
| docid_to_idx = {int(corpus_ds[i]["doc_id"]): i for i in range(len(corpus_ds))} | |
| corpus = Corpus(corpus_data=corpus_ds, docid_to_idx_mapping=docid_to_idx, doc_column_name="image") | |
| rename = {} | |
| if "positive_doc_id" in queries_ds.column_names: | |
| rename["positive_doc_id"] = "pos_target" | |
| if "negative_doc_ids" in queries_ds.column_names: | |
| rename["negative_doc_ids"] = "neg_target" | |
| for old, new in rename.items(): | |
| if old in queries_ds.column_names and new not in queries_ds.column_names: | |
| queries_ds = queries_ds.rename_column(old, new) | |
| if "pos_target" not in queries_ds.column_names: | |
| raise ValueError("hardneg queries need positive_doc_id / pos_target") | |
| min_negs = max(1, int(num_negatives)) if use_negatives else 1 | |
| if "neg_target" in queries_ds.column_names: | |
| before = len(queries_ds) | |
| lengths = pc.list_value_length(queries_ds.data.column("neg_target")) | |
| valid = pc.fill_null(pc.greater_equal(lengths, min_negs), False) | |
| n_valid = int(pc.sum(pc.cast(valid, pa.int64())).as_py()) | |
| if n_valid != before: | |
| queries_ds = queries_ds.select(pc.indices_nonzero(valid).to_pylist()) | |
| print( | |
| f"[data] hardneg {before} -> {len(queries_ds)} (>= {min_negs} negs); " | |
| f"corpus={len(corpus_ds)} from {corpus_origin}" | |
| ) | |
| if use_negatives: | |
| if "neg_target" not in queries_ds.column_names: | |
| raise ValueError("hardneg queries need negative_doc_ids / neg_target") | |
| wrapped = ColPaliEngineDataset( | |
| queries_ds, | |
| corpus=corpus, | |
| query_column_name="query", | |
| pos_target_column_name="pos_target", | |
| neg_target_column_name="neg_target", | |
| num_negatives=num_negatives, | |
| ) | |
| _attach_task_ids(wrapped, queries_ds, corpus_ds, docid_to_idx) | |
| return wrapped | |
| print(f"[data] hardneg in-batch only: queries={len(queries_ds)}") | |
| wrapped = ColPaliEngineDataset( | |
| queries_ds, | |
| corpus=corpus, | |
| query_column_name="query", | |
| pos_target_column_name="pos_target", | |
| neg_target_column_name=None, | |
| num_negatives=0, | |
| ) | |
| _attach_task_ids(wrapped, queries_ds, corpus_ds, docid_to_idx) | |
| return wrapped | |