Skip to main content

MulTaBench: Benchmarking Multimodal Tabular Learning

Work in progress: MulTaBench is moving towards a living leaderboard, and master will change gradually on the way there. To reproduce the paper, use the tag paper_version or the official-paper-version branch. The paper's results are kept in multabench/leaderboard/legacy_results/ while every model is rerun under the leaderboard protocol.

A benchmark for multimodal tabular learning: tables whose columns include images or free text, not just numbers and categories. It is 20 image-tabular and 20 text-tabular curated datasets, with 40 further datasets released alongside them, 80 in total. The benchmark evaluates tabular learners under a target-aware setting, where the image and text encoders are fine-tuned on the task rather than used frozen.

Paper: MulTaBench: Benchmarking Multimodal Tabular Learning with Text and Image (NeurIPS 2026 Spotlight)
Datasets: huggingface.co/multabench

Leaderboard

Default hyperparameters, --size 10k, 6 folds, frozen e5-small text and dino-small image embeddings (ConTextTab reads raw text itself). Elo is fit on one battle per dataset and model pair, with RandomForest at 1000 and 95% intervals from bootstrapping the datasets; a model with no result on a dataset has no battles there. TabPFN-v2 and v2.5 take at most 10 classes, which leaves out 3 of the 80 datasets.

# Model Elo, 80 datasets 95% CI Text, 40 Images, 40 Datasets
1 TabPFN-3.5 2221 +230 / −176 2080 2458 80
2 Kumo-Tabular-Small 2139 +190 / −151 2094 2268 80
3 TabPFN-3.5-Fast 2010 +195 / −149 1906 2199 80
4 TabICLv2 1745 +137 / −115 1637 1941 80
5 TabPFN-3 1666 +145 / −117 1554 1866 80
6 TabPFN-v2p5 1661 +136 / −114 1647 1743 77
7 TabPFN-v2 1407 +117 / −97 1322 1559 77
8 TabM 1366 +98 / −89 1348 1432 80
9 ConTextTab 1353 +113 / −102 1279 1490 80
10 CatBoost 1298 +66 / −55 1307 1321 80
11 TabDPT 1251 +112 / −93 1116 1455 80
12 RealMLP 1082 +81 / −71 1103 1059 80
13 LightGBM 1058 +63 / −61 1093 1013 80
14 RandomForest 1000 anchor 1000 1000 80
15 XGBoost 988 +67 / −65 1024 936 80

Each model's runs are in multabench/leaderboard/models/<model>.csv, one row per (dataset, fold), failed runs included. python -m multabench.leaderboard.elo prints the table, with win rate, average rank and normalized error; --modalities text or --modalities image,image-text restricts it to those datasets.

Leaderboard runs pass --official, which also requires a clean checkout of a commit on master, so the git column of every row names code anyone can check out. The image runs of the first 11 models are the one exception: they ran at 7f28e55, on the branch of #148, which was squash-merged unchanged.

Getting started

To use the datasets from another project, install the core, which needs neither torch nor any model package:

pip install multabench
from multabench import Size, load_embeddings, load_split
split = load_split("MUL_IMAGE_PETFINDER", fold=0, size=Size.TEN_K)  # split.x_train, split.y_train, split.x_test, ...
embeddings = load_embeddings("MUL_TEXT_WINE_REVIEW", fold=0, size=Size.TEN_K, encoder="e5-small")

To run the benchmark:

source init.sh && source .venv/bin/activate
cp .env.example .env     # Hugging Face token

python benchmark.py --model light --dataset_name MUL_IMAGE_PETFINDER --fold 0 --text_encoder e5-small --image_encoder dino-small

pyproject.toml lists the core dependencies, a bench extra for benchmark.py and one extra per model package, so pip install -e ".[bench,catboost]" installs what one model needs; each wrapper imports its package only when it runs. ConTextTab and Kumo are pinned to a git commit, which PyPI doesn't allow in extras, so requirements-git.txt lists them. init.sh sets up the shared .venv from requirements.txt, which locks the core, every extra and those two.

benchmark.py is the single entry point: it evaluates one model on one dataset fold and writes the result to runs/<model>_<dataset>_<text_encoder>_<image_encoder>_<size>_<fold>.json (--output_dir to change), with none for an encoder the dataset has no column for. A run that fails still writes its JSON, with "status": "error" and the error, and exits non-zero; a successful one has "status": "ok". A run whose JSON already exists is skipped, so a stopped sweep can be relaunched as is; --overwrite reruns it. --help lists the available models. --text_encoder picks how text columns are embedded: tfidf, frozen e5-small (the default) or e5-large, or e5-small-tar, E5 fine-tuned on the task with LoRA (target-aware). --image_encoder does the same for image columns: frozen dino-small (the default) or dino-large, or dino-small-tar. Each applies only to datasets with that kind of column.

sweep.py runs benchmark.py over models × datasets × encoders × folds. It writes the runs that don't have a result yet to a jobs file (sweep_jobs_<timestamp>.txt, or --jobs_file), one command per line, which any scheduler can run. --modalities text, image or image-text keeps only those datasets. Text datasets run once per text encoder and image datasets once per image encoder; image datasets that also have text columns always embed their text with frozen e5-small. The default encoders include the fine-tuned e5-small-tar and dino-small-tar, so pass --text_encoders/--image_encoders to run fewer. Each run is its own process, and a failed run doesn't stop the others:

python sweep.py --models light,cat --modalities text --text_encoders e5-small                   # only write the jobs file
python sweep.py --models light,cat --modalities text --text_encoders e5-small --launcher local  # run them here, one after another
python sweep.py --models light,cat --modalities text --text_encoders e5-small --launcher slurm \
    --sbatch="--partition=cpu --cpus-per-task=8 --mem=32G" --submit
python sweep.py --models tabm,real --modalities text --text_encoders e5-small --launcher slurm \
    --sbatch="--partition=gpu --gres=gpu:1 --cpus-per-task=8 --mem=32G" --submit

With --launcher slurm, each line becomes a task of a Slurm array; --sbatch="..." is passed to sbatch as is (the = is needed, since the value starts with --), and without --submit the sbatch commands are only printed. With a launcher, the sweep first downloads the runs' datasets and cached embeddings, since runs that download the same files at once into a shared Hugging Face cache (e.g. on NFS) can fail. All the runs of a sweep get the same --sbatch, so sweep the runs that need a GPU apart from those that don't (see below), as in the last two commands. Relaunching a sweep runs only what is still missing.

Every leaderboard run must use the same hardware, so their times compare: one NVIDIA H100 (80 GB), an Intel Xeon Platinum 8481C with 8 CPU cores and 32 GB of RAM (GPU_RUN_HARDWARE in multabench/utils/hardware.py). Runs that need no GPU, a CPU model (CatBoost, LightGBM, XGBoost, RandomForest) whose text is embedded with TF-IDF or read from the cache, use an Intel Xeon Platinum 8581C with 8 CPU cores and 32 GB of RAM instead (CPU_RUN_HARDWARE). With --official (sweep.py --official passes it to every run), benchmark.py checks the GPU, the processor, the CPUs it may use and its memory limit, and raises before running if any differs; leaderboard runs always pass it. On Slurm, request --gres=gpu:1 --cpus-per-task=8 --mem=32G on an H100 node, or --cpus-per-task=8 --mem=32G on an 8-core Xeon 8581C node for CPU runs. The sweeps' Slurm jobs run under srun, so a run sees only the CPUs its job asked for, even when Slurm hands it a whole machine. The cached embeddings are computed on one NVIDIA A100, 6 CPU cores and 32 GB of RAM for now (EMBEDDING_HARDWARE); the hardware they ran on is recorded with them.

collect_results.py gathers the run JSONs into one CSV per model in multabench/leaderboard/models/, one row per run, failed runs included: model, dataset, encoders, size and fold, the status, the metric with test_score (higher is better) and test_error, train and inference time per 1K rows apart from embedding, embedding time per 1K rows, and the hardware. A run already in a model's file is replaced by its rerun; the other rows are kept.

python collect_results.py --output_dir runs

To use a benchmark split in your own code, load_split downloads the dataset and returns the same train and test rows that benchmark.py evaluates on:

from multabench import Size, load_split

split = load_split("MUL_IMAGE_PETFINDER", fold=0, size=Size.TEN_K)
split.x_train, split.y_train, split.x_test, split.y_test, split.task_type, split.image_folder, split.image_column

Cached embeddings

The frozen text and image embeddings are cached on Hugging Face, one public dataset repo per encoder: multabench/embeddings-e5-small and multabench/embeddings-dino-small. A frozen encoder doesn't depend on the split, so each distinct input of a dataset is embedded once, under <dataset>/, in embeddings.npz, which maps every input, exactly as the encoder got it (e5's "passage: <column>: <value>" after a run's missing-text fill, or the image file name), to its raw encoder output (PCA is fitted per run, on the training rows), and meta.json, with the dataset's rows and columns, the encoding time per feature, the hardware it ran on, the hardware it should run on (EMBEDDING_HARDWARE in multabench/utils/hardware.py) and how they differ. A run looks its own inputs up, so an embedding is never paired with another row's text, and an input missing from the cache, e.g. after the dataset changed, raises. The cache is optional: benchmark.py reads it when the dataset has embeddings for the encoder, and otherwise embeds live, so new encoders and datasets need no cache to run. Only the frozen e5-small text embeddings are read so far. load_embeddings returns them aligned with load_split's rows:

from multabench import Size, load_embeddings

embeddings = load_embeddings("MUL_TEXT_WINE_REVIEW", fold=0, size=Size.TEN_K, encoder="e5-small")
embeddings.train["description"], embeddings.test["description"], embeddings.meta

python -m multabench.embeddings.embed embeds one dataset, and python -m multabench.embeddings.embed_sweep runs it over encoders × datasets, with the same --launcher and --sbatch options as sweep.py, and uploads the result with --upload:

python -m multabench.embeddings.embed_sweep --launcher slurm --sbatch="--partition=a100 --gres=gpu:1 --cpus-per-task=6 --mem=32G" --submit
python -m multabench.embeddings.embed_sweep --upload

Datasets

Every dataset is a public dataset repo in the multabench Hugging Face organization, downloaded on demand. Repos are named <core|extended>-<img|text>-<reg|cls>-<name>; core and extended are two slices of the benchmark. Each holds data.parquet with explicit column types (datetime, categorical, numeric, string), metadata.json with the target, and, for image datasets, the images in images-*.zip.

The paper used the curated CSV copies on the chico89 Kaggle account, which the Hugging Face datasets were converted from. The two may differ: the Hugging Face datasets store dates, categories and numbers as typed columns where the CSVs hold strings, and a few non-numeric values in numeric columns were mapped (DVM car's "1 mile" to 1, Anime Planet's "Unknown" to missing). Scores on the two versions are not directly comparable.

What you are looking for Where it is
A dataset's Hugging Face repo the MulTaBenchDatasetID values in multabench/datasets/all_datasets.py
Where a dataset originally comes from MULTABENCH_SOURCES in multabench/datasets/all_datasets.py
How a dataset was curated the recipes in multabench/benchmark/datasets/ at the paper_version tag
Size, task and feature counts per dataset multabench/leaderboard/results/datasets_summary{,_extra}.csv at the paper_version tag

The 40 datasets released alongside the benchmark were admitted on a weaker criterion and carry no tier name of their own, so their scores must not be pooled with the 40 MulTaBench datasets.

Protocol and TabArena

MulTaBench follows the TabArena protocol where doing so is cheap or affects correctness, and deliberately keeps its own choices where full parity would multiply the compute. MulTaBench is not an official TabArena leaderboard.

TabArena MulTaBench Status
Metrics ROC AUC (binary), log loss (multiclass), RMSE (regression), via AutoGluon's scorers Same ✅
Leaderboard Elo via bencheval, RandomForest (default) anchored at 1000, missing results imputed with RandomForest and flagged, bootstrapped confidence intervals Elo as in the paper, RandomForest anchored at 1000, bootstrapped confidence intervals; missing results are left out, neither imputed nor dropping the model, so a model is ranked on the datasets it ran on, which its Datasets column shows
Splits Fixed and part of the task definition Same: a seeded function of the data ✅
Group and time structure Audited per dataset; group-aware or forward-in-time splits where needed Not audited: every dataset is split IID. Some datasets may have group or time structure, in which case IID splits can overestimate performance
Runtime Train and inference time per 1K rows Same, with the time spent embedding text and images reported separately ✅
Hardware Recorded per run; 8 CPU cores and 32 GB of RAM, with one NVIDIA L40S (48 GB) for GPU models Recorded per run; every run uses 8 CPU cores and 32 GB of RAM, GPU models with one NVIDIA H100 (80 GB), checked by benchmark.py --official
Outer splits 3 folds, repeated 1–10 times depending on dataset size 3 folds, repeated twice for every size
Dataset size Full size --size 10k (the default, and the one the leaderboard reports) caps train and test at 10K and 5K rows; --size full keeps every row
Inner validation 8-fold bagging for every model No bagging: one fit per split, and models that need validation hold out 10% of the training rows (at most 1,000). This favors models that ensemble internally (TabPFN, TabICL, TabDPT, TabM, RandomForest) over single GBDTs and RealMLP
Hyperparameters Default, tuned, and tuned + ensembled Default only
Model implementations TabArena's model registry MulTaBench's own wrappers, which add image and text embeddings

Metadata

Release files for multabench 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for multabench 0.0.1
File Size Uploaded Provenance
multabench-0.0.1.tar.gz 87.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for multabench 0.0.1
File Interpreter ABI Platform
multabench-0.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 183.0 kB

Release files / multabench-0.0.1.tar.gz

Download URL multabench-0.0.1.tar.gz
Size 87.2 kB
Tags Source
SHA-256 checksum
How to use checksums
60c7db74455aab3d765188f6781137aaf8711d8953a5649601d1fd9ebdb0fcc1
BLAKE2b-256 checksum
How to use checksums
73824cce93fdd228199263041af020ee005a0dbfadb45ff7a1450f9d25c40fab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Release files / multabench-0.0.1-py3-none-any.whl

Download URL multabench-0.0.1-py3-none-any.whl
Size 95.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9c95fd1f4a4f0e2088d6c08e4cf2d4fa38fb958ce00599c98bd40ec3607396c8
BLAKE2b-256 checksum
How to use checksums
3543968a69912089d710236a79b37386b0d407632d5cc7ca0f35299384df7f36
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Security for multabench 0.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page