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 SentenceTransformer model = SentenceTransformer("tencent/EVIE-4.5B") sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Notebooks
- Google Colab
- Kaggle
| """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 | |