Skip to content

Repository files navigation

Predicting Algorithm Runtime Distributions: An In-Context Learning Approach with TabPFN

Code for a study on predicting the runtime distributions (RTDs) of randomised algorithms. We assess Prior-Data Fitted Networks, specifically TabPFN [4], on this task and compare it against established baselines: Random Forests and Gaussian Processes [1], DistNet [2], and Bayes DistNet [3]. Our TabPFN approach sets a new state of the art for this problem.

This repository accompanies the AutoML 2026 paper “From Tables to Runtime: Predicting Algorithm Runtime Distributions with TabPFN.”

If you use this code in your research, please cite:

@inproceedings{ibrahimli2026tables,
  title     = {From Tables to Runtime: Predicting Algorithm Runtime Distributions with {TabPFN}},
  author    = {Ibrahimli, Hagverdi and Eggensperger, Katharina and Adriaensen, Steven},
  booktitle = {Proceedings of the Fifth International Conference on Automated Machine Learning},
  year      = {2026},
  series    = {Proceedings of Machine Learning Research},
  publisher = {PMLR}
}

Carried out at the AutoML Lab, University of Freiburg, with the codebase implemented primarily by Hagverdi Ibrahimli as part of his MSc. research project.

Contents

Motivation

Runtime distributions are typically multi-modal and heavy-tailed, so standard regression is not enough.

An empirical runtime distribution, showing its multi-modal and heavy-tailed shape.

We use TabPFN to fit a posterior predictive distribution over runtime, which captures this structure.

TabPFN vs. Oracle Normal on a bimodal log runtime distribution (from spear_qcp). The unimodal Oracle Normal (red) misses the second mode; TabPFN's posterior predictive distribution (green) captures both, against the observed runtimes (blue crosses).

Task

A randomised solver does not have one runtime per instance, it has a distribution over runtimes. Given the feature vector of an unseen instance, the goal is to predict that full distribution.

Every instance in the benchmark was attempted 100 times with independent random seeds, each attempt bounded by the scenario's cutoff. Those 100 measurements are its empirical RTD. A model trains on the instances of a training split, predicts a distribution for each held-out test instance, and is scored against that instance's 100 observed runtimes rather than their mean.

Data

We use the benchmark released with DistNet [2], fetched by the script in Downloading the data.

A scenario pairs one randomised algorithm with one family of problem instances. It provides a numeric feature vector per instance (the inputs), 100 measured runtimes per instance (the targets), and the captime used when the data was generated. Scenario names and their on-disk paths are registered in data_source_release.py.

load_data.py reproduces the original DistNet cleaning. An instance is dropped if any of its runs crashed or timed out, if any runtime reached the cutoff (a censored measurement), if its feature vector is constant, or if its solver status falls outside the scenario's class. Every surviving instance therefore has 100 uncensored measurements, which is why the counts below are lower than in the raw archive. The -512 missing-value sentinel is replaced by the column median.

Scenario Domain # Instances # Features Cutoff (s)
clasp_factoring SAT 2000 102 5000
saps-CVVAR SAT 10011 46 60
spear_qcp SAT 8072 91 5000
yalsat_qcp SAT 11743 91 5000
spear_swgcp SAT 11182 76 5000
yalsat_swgcp SAT 11182 76 5000
lpg-zeno Planning 3999 165 300

Counts are after cleaning. One cross-validation fold trains on about 90% of them, which matters when choosing --context_size under --subsample_unflattened.

Models

All models solve the same task and are scored by the same code. They differ in which distributions they can express and how they are fitted.

Model --model Predicted distribution Fitted by Multi-modal
TabPFN [4] tabpfn Learned piecewise density over bucket borders In-context, one forward pass, no training Yes
DistNet [2] distnet Log-normal, or normal over log-runtime SGD, maximum likelihood No
Bayes DistNet [3] bayesian_distnet Log-normal fitted to samples from a weight posterior Bayes-by-Backprop No
Random Forest [1] random_forest Normal over log-runtime, from mean and predictive variance Forest fit, optional SMAC tuning No
Gaussian Process [1] gp Normal over log-runtime Exact GP, marginal likelihood No
Log-normal lognormal Single log-normal, features ignored Closed-form moments No
  • TabPFN is the method under study. It is never trained on this data: the training rows are passed as context and one forward pass gives a predictive distribution per test instance. That distribution is a learned piecewise density, not a member of a two-parameter family, so it more reliably captures the bimodal RTDs that the baselines struggle with. See tabpfn_helpers.py.
  • DistNet is the reference neural baseline: a two-hidden-layer MLP (16 units, tanh, batch norm) predicting the two parameters of a log-normal. See distnet_lognormal.py.
  • Bayes DistNet replaces DistNet's dense layers with Bayes-by-Backprop layers. Each forward pass samples one runtime; n_ens passes define the predictive distribution. It targets better calibration when few observations per instance are available. See bayesian_distnet.py.
  • Random Forest is made distributional by the Law of Total Variance: the mean of the per-leaf variances plus the variance across tree means gives a predictive variance alongside the mean. It needs the patched scikit-learn that draws exact split points uniformly at random. See random_forest.py.
  • Gaussian Process uses a scaled Matern-3/2 kernel plus a white-noise term, which absorbs the run-to-run variability of identical feature rows. Exact inference is cubic in the number of training rows, making it the bottleneck of the context-size sweep.
  • Log-normal ignores the features and fits one distribution to the pooled training runtimes. It is the floor a feature-based model must beat, and is not part of the main comparison.

Oracle references

--oracle ignores the instance features and fits each test instance's own 100 observed runtimes. This upper-bounds what a model of that family can score on this data.

  • --model tabpfn --oracle fits TabPFN on the test instance's own runtimes, using constant dummy features.
  • --model lognormal --oracle uses the per-instance mean and standard deviation of the observed log-runtimes.

Comparing the two oracles isolates the cost of the unimodal parametric form, since both are given the same information.

Target scaling

Runtimes span orders of magnitude, so every model is fitted on a transformed target chosen with --target_scale. The metric code inverts the transform, including the density's Jacobian, so scores from different target scales are directly comparable. This is what makes the scaler ablations meaningful.

--target_scale Transform Supported by
log log1p(y) tabpfn, distnet, random_forest (required), gp (required)
max y / max(y_train), min-max with an implicit minimum of 0 tabpfn, distnet, bayesian_distnet (required)
original none tabpfn

lognormal always works on natural log-runtime, but the CLI still requires the flag.

Metrics

All metrics are lower-is-better, computed per test instance and then aggregated. See calculate_metrics.py.

Each prediction is first mapped into one shared space, z = log1p(y), so models fitted under different target scales are compared on equal terms. The distributional metrics are then integrated numerically on a per-instance grid of 15,000 points covering the union of the empirical support and the model's own (0.0001, 0.9999) quantile range, so neither an over-dispersed nor an over-confident prediction is truncated.

Metric Definition
NLLH Primary metric. Mean negative log-density of the 100 observed runtimes, normalised per instance by that instance's largest observed log-runtime. Rescaling the log-runtime axis shifts a log-density by a constant, and this constant makes NLLH comparable across instances and scenarios whose runtimes differ by orders of magnitude. Densities are floored at 1e-10.
CRPS Continuous ranked probability score against the 100 observations, using the exact decomposition ∫(F - F_emp)² + ∫F_emp(1 - F_emp). The second term is the intrinsic spread of the sample, a floor no model can beat.
Wasserstein 1-Wasserstein distance, the integrated absolute CDF gap.
KS Kolmogorov-Smirnov statistic, the largest absolute CDF gap.
MAE Absolute difference between predicted and empirical median, in seconds. Reported for reference; it rewards central tendency and ignores distributional shape.

Experimental protocol

Cross-validation. A single KFold(n_splits=10, shuffle=True, random_state=0) splits instances, never individual runs, so all 100 runtimes of an instance stay on one side and a test instance is genuinely unseen. The seed is fixed project-wide, so fold k holds the same instances for every model and scenario. Per-fold scores are therefore paired across models, which is what the signed-rank test used for context-size selection relies on.

Repetitions. Any run involving random subsampling (of context, features, or per-instance samples) is repeated with 5 seeds.

Aggregation. Every plot reduces scores in this order:

  1. average the per-test-instance values within a run;
  2. average over the repetition seeds, collapsing replicates that share a fold;
  3. average over the 10 folds for the plotted mean, and take the standard deviation across folds for the shaded band.

Averaging seeds before the fold statistics keeps the error bands a measure of cross-fold variability rather than of resampling noise.

Compute budgets. DistNet training and Random Forest tuning are each capped at one hour of wall-clock time per run, so the baselines get comparable compute. Epoch and trial counts are only safety caps. A curve ending in an x marker means the model hit its time limit or ran out of memory there.

One CLI invocation runs one (model, scenario, fold) point. Sweeping the grids is the caller's job, typically a cluster array job.

Installation

The environment is defined in pyproject.toml and pinned by the committed uv.lock.

Platform requirements

Linux is the supported platform and the one the reported experiments ran in. Some dependencies are compiled from source at install time, so a C++ toolchain must be available:

Platform Requirement
Linux gcc and g++.
Windows Visual Studio Build Tools with the Desktop development with C++ workload, or WSL2.
macOS Xcode command line tools (xcode-select --install).

Steps

  1. Install uv:

    pip install uv
  2. From the repository root:

    uv sync

This installs the locked dependency versions, including the patched forks of TabPFN and scikit-learn pinned to exact commits, and PyTorch for CUDA 13.0. uv fetches CPython 3.13.0 automatically if needed.

Two notes:

  • The TabPFN fork provides the predictive distributions this project consumes: predict(..., output_type="full") must return the bucket logits together with the bar-distribution criterion. The scikit-learn fork draws exact split points uniformly at random, as the Random Forest of [1] requires.
  • uv sync also installs the dev group (pytest, black, ipykernel). ipykernel is what lets the notebooks run. Use uv sync --no-dev to skip it.

A GPU is strongly recommended for TabPFN, and a cluster or batch environment for the full experiment grid.

Downloading the data

The benchmark archive is not redistributed here. From the repository root:

python download_distnet_data.py

This extracts the data into data/distnet_data/.

Repository structure

project_root_dir/
├── download_distnet_data.py # Downloads and extracts DistNet benchmark datasets
├── pyproject.toml           # Package and dependency configuration (uv)
├── uv.lock                  # Fully resolved dependency versions
├── data/distnet_data/       # Benchmark data, created by the download script
├── notebooks/               # Analysis notebooks producing the figures
│ ├── feature_robustness.ipynb
│ ├── heatmap_factorial_design.ipynb
│ ├── predictive_performance_scaling.ipynb
│ └── wilcoxon_signed_rank_test.ipynb
├── results/                 # Per-run metadata and compiled .pkl/.csv results
└── src/
   └── tabpfn_project/
     ├── experiment_config.py # Typed description of a run; documents every knob
     ├── globals.py           # Scenarios, metrics, scales, and budgets
     ├── paths.py             # Project root and key directories
     ├── helper/
     │ ├── bayesian_distnet.py  # Bayes DistNet [3]
     │ ├── calculate_metrics.py # NLLH, CRPS, Wasserstein, KS, MAE
     │ ├── data_source_release.py # Scenario registry
     │ ├── distnet_lognormal.py # DistNet [2]
     │ ├── load_data.py         # Loading, cleaning, and KFold splitting
     │ ├── preprocess.py        # Instance filters, imputation, standardisation
     │ ├── random_forest.py     # Random Forest baseline [1]
     │ ├── tabpfn_helpers.py    # Batched and oracle TabPFN inference
     │ ├── utils.py             # Subsampling, aggregation, and plotting
     │ └── y_scalers.py         # Target transforms
     └── scripts/
       ├── main.py            # CLI entrypoint
       ├── model_handler.py   # Per-model training and evaluation
       └── prepare_data.py    # Builds train/test arrays for a run

Each run writes one pickle to results/<save_dir>/metadata/<experiment_id>_metadata.pkl holding its full configuration and metrics. TabPFN also writes its raw predictive distributions to results/<save_dir>/tabpfn_preds_full/, which are too large for the metadata. helper.utils.fetch_save_dict compiles a metadata directory into one flat .pkl list, which is what the notebooks load.

generate_experiment_id does not encode --do_hpo or --rf_new_default. Two Random Forest runs differing only in those flags produce the same filename and overwrite each other, so every model variant needs its own --save_dir.

Usage

Run a single fold from the repository root:

python -m tabpfn_project.scripts.main \
 --scenario lpg-zeno \
 --model tabpfn \
 --fold 0 \
 --context_size 128 \
 --seed_context_size 100 \
 --target_scale log \
 --save_dir test_run_dir

Command-line reference

Each flag maps to one attribute of ExperimentConfig, whose docstring gives the full semantics.

Required

Flag Type Description
--scenario choice One of the seven scenarios above.
--model choice distnet, tabpfn, bayesian_distnet, random_forest, lognormal, gp.
--fold int 0-9 Cross-validation fold index.
--target_scale choice log, max, or original. See the support matrix above.
--save_dir str Sub-directory of results/ for this run's output.

Training set

Flag Type Default Description
--context_size int all Size of the training set given to the model. Counts rows of the flattened table, or instances with --subsample_unflattened. Requires --seed_context_size.
--seed_context_size int Seed for the context subsampling.
--subsample_unflattened flag off Subsample whole instances before flattening, without replacement, giving exactly context_size * num_samples_per_instance rows. Without it, rows are drawn after flattening with replacement, which allows context sizes beyond the natural training-set size.
--num_samples_per_instance int 1-100 100 Runtimes kept per training instance. Requires --seed_samples_per_instance when below 100. Test targets are never subsampled.
--seed_samples_per_instance int Seed for the per-instance runtime subsampling.
--remove_duplicates flag off TabPFN only. Keep one runtime per training instance, collapsing the duplicate feature rows.

Features

Flag Type Default Description
--n_features_keep int all Number of randomly chosen features to keep. Takes precedence over --feature_drop_rate. 0 is meaningful: features become a single zero column, giving a featureless baseline that can only learn the scenario's marginal RTD.
--feature_drop_rate float 0-1 Fraction of features to drop. Ignored if --n_features_keep is given.
--seed_feature_drop_rate int Seed for feature subsampling. Required with either flag above.
--jitter_x flag off TabPFN diagnostic. Adds zero-mean Gaussian noise to the training features. Requires --jitter_val. Skipped in oracle mode.
--jitter_val float Relative noise intensity: column j gets noise of std jitter_val * std(column j).
--rand_extend_x flag off TabPFN diagnostic. Appends noise columns to train and test features, breaking exact row duplication without adding signal. Requires --n_rand_cols. Skipped in oracle mode.
--n_rand_cols int Number of random columns to append.

Model behaviour and compute

Flag Type Default Description
--early_stopping flag off DistNet and Bayes DistNet. Holds out 20% of the training data with a GroupShuffleSplit on instance ids and restores the best-validation checkpoint.
--do_hpo flag off Random Forest. Tunes with SMAC over a 3-fold GroupKFold NLLH objective, budgeted at one hour. Supersedes --rf_new_default.
--rf_new_default flag off Random Forest. Uses n_estimators=50, var_min=1e-6 instead of the values of [1] (10, 0.01).
--oracle flag off TabPFN and lognormal only. Fits the diagnostic upper bound described above.
--use_cpu flag off Forces CPU execution even when CUDA is available.

Reproducing the results

python -m tabpfn_project.scripts.main [args]

Experiment 1: predictive performance scaling

Sweeps the context size for every model, showing how predictive quality scales with the amount of training data.

Base arguments: --scenario, --model, --fold, --context_size, --seed_context_size, --save_dir. Results land in RESULTS_DIR/<save_dir>.

Grid:

  • --context_size: 2**i for i in range(5, 17); for gp the maximum i is 12
  • --seed_context_size: i * 100 for i in range(1, 6)
  • --fold: range(10)

Run each variant below across that grid, giving each its own --save_dir:

  • TabPFN
    • Variant 0: --target_scale log
    • Variant 1: --target_scale max
    • Variant 2 (noDUPS): --target_scale log --remove_duplicates
    • Variant 3 (naive): --target_scale log --n_features_keep 0 --seed_feature_drop_rate -1
  • DistNet
    • Variant 0: --early_stopping --use_cpu --target_scale max
    • Variant 1: --early_stopping --use_cpu --target_scale log
  • Random Forest
    • Variant 0: --use_cpu --rf_new_default --target_scale log
    • Variant 1: --use_cpu --rf_new_default --target_scale log --do_hpo
    • Variant 2: --use_cpu --target_scale log
  • Bayes DistNet
    • Variant 0: --early_stopping --use_cpu --target_scale max
  • GP
    • Variant 0: --use_cpu --target_scale log

Experiment 2: feature robustness

Sweeps the number of retained features down to zero, measuring how much each model exploits instance features rather than the scenario's marginal runtime distribution. Requires Experiment 1 to be complete.

First extract the optimal context size per variant, so each model is compared at its own operating point. Taking the best mean score would overfit fold noise and favour the largest context tried, so instead each candidate is compared to the empirical best with a one-sided Wilcoxon signed-rank test over the 10 paired per-fold scores, and the smallest candidate that is not significantly worse is chosen.

from tabpfn_project.helper.utils import fetch_save_dict, analyze_optimal_context
from tabpfn_project.paths import RESULTS_DIR
from tabpfn_project.globals import DISTNET_SCENARIOS

# 1. Compile the per-run metadata into one list (repeat per variant)
fetch_save_dict(
   results_dir=RESULTS_DIR / "model_save_dir_name",
   metadata_dir=RESULTS_DIR / "model_save_dir_name" / "metadata",
   model_name="tabpfn",           # matches the run's --model
   model_file_name="tabpfn_var0", # output filename stem, distinguishes variants
   scenario=None,
)

# 2. Extract optimal context sizes to CSV (repeat per variant)
analyze_optimal_context(
   model_results_path=RESULTS_DIR / "model_save_dir_name" / "tabpfn_var0.pkl",
   scenarios=DISTNET_SCENARIOS,
   metric="NLLH",
   output_csv_path=RESULTS_DIR / "model_save_dir_name" / "tabpfn_var0.csv",
)

Do this for four variants: TabPFN 0, DistNet 0, Random Forest 0, and GP 0.

Then run those four with the extracted --context_size.

Base arguments: --scenario, --model, --fold, --context_size (from the CSV), --seed_context_size, --save_dir, --n_features_keep, --seed_feature_drop_rate

Grid:

  • --seed_context_size: i * 100 for i in range(1, 6)
  • --seed_feature_drop_rate: i * 1000 for i in range(1, 6)
  • --n_features_keep: per scenario, start at 0, then 1, doubling up to the scenario's feature count without passing it, then append the exact feature count. For 12 features: [0, 1, 2, 4, 8, 12]. At 0 and at the full count, use the single seed --seed_feature_drop_rate -1.

Experiment 3: TabPFN factorial design

Crosses the number of training instances with the number of runtime samples per instance, separating whether performance comes from seeing more instances or from seeing each instance more often. TabPFN variant 0 only.

Base arguments: --scenario, --model, --fold, --context_size, --seed_context_size, --save_dir, --subsample_unflattened, --num_samples_per_instance, --seed_samples_per_instance

Grid:

  • --seed_context_size: 200, 400, 600
  • --seed_samples_per_instance: 2000, 4000, 6000
  • --num_samples_per_instance: [2**i for i in range(0, 7)] + [100]
  • --context_size: 1-2-5 magnitude scaling, 10, 20, 50, 100, 200, 500, 1000, ..., up to the scenario's instance count

Under --subsample_unflattened, --context_size counts instances drawn without replacement, so it must not exceed the fold's training instances, about 90% of the totals in the table above. For the full-data point at the top of each heatmap, omit --context_size and --seed_context_size; plot_ml_heatmap places such runs at their effective instance count.

Figures

The notebooks in notebooks/ build the figures from the compiled .pkl files. Each figure-producing cell is marked with a # Figures ... comment on its first line.

Cell references give the cell index (0-based position in the .ipynb cells array) and the code-cell ordinal (what the execution counter shows after a clean top-to-bottom run).

Figure Notebook Cell index Code cell Plotting function
1, 4, 7 predictive_performance_scaling.ipynb 18 9 plot_main_results
2 predictive_performance_scaling.ipynb 21 11 plot_instance_level_bar_chart
3(a), 8 predictive_performance_scaling.ipynb 7 4 plot_main_results
3(b), 11 heatmap_factorial_design.ipynb 5 3 plot_ml_heatmap
5, 10 feature_robustness.ipynb 5 3 plot_feat_dropping_results
6, 9 predictive_performance_scaling.ipynb 25 13 plot_combined_time_vram_results
12 predictive_performance_scaling.ipynb 9 5 plot_main_results
13 predictive_performance_scaling.ipynb 5 3 plot_main_results
14 predictive_performance_scaling.ipynb 11 6 plot_main_results
15 predictive_performance_scaling.ipynb 13 7 plot_main_results
16 predictive_performance_scaling.ipynb 15 8 plot_main_results

Files the notebooks load

The notebooks read fixed paths, so the model_file_name and results_dir passed to fetch_save_dict must match these.

predictive_performance_scaling.ipynb, from Experiment 1:

File under results/ Variant
tabpfn/tabpfn_scalerLOG.pkl TabPFN 0
tabpfn/tabpfn_scalerMAX.pkl TabPFN 1
tabpfn/tabpfn_scalerLOG_noDUPS.pkl TabPFN 2
tabpfn/tabpfn_naive.pkl TabPFN 3
tabpfn/tabpfn_oracle.pkl TabPFN with --oracle
distnet/distnet_scalerMAX.pkl DistNet 0
distnet/distnet_scalerLOG.pkl DistNet 1
bayesian_distnet/bayesian_distnet_scalerMAX.pkl Bayes DistNet 0
random_forest/random_forest_newDEFS.pkl Random Forest 0
random_forest/random_forest_tuned.pkl Random Forest 1
random_forest/random_forest_oldDEFS.pkl Random Forest 2
gaussian_process/gp_scalerLOG.pkl GP 0

feature_robustness.ipynb, from Experiment 2: tabpfn/tabpfn_fdrop.pkl, distnet/distnet_fdrop.pkl, random_forest/random_forest_fdrop.pkl, gaussian_process/gp_fdrop.pkl.

heatmap_factorial_design.ipynb, from Experiment 3: tabpfn/tabpfn_heatmap.pkl.

wilcoxon_signed_rank_test.ipynb runs the context-size selection for Experiment 2 and writes its CSV. The committed cell is a worked example pointing at results/GaussianProcess/gp_results_exp_random_subsample_log.pkl; adapt the paths to your own --save_dir names.

References

  1. Hutter, F., Xu, L., Hoos, H. H., & Leyton-Brown, K. (2014). Algorithm Runtime Prediction: Methods & Evaluation. Artificial Intelligence, 206, 79-111.
  2. Eggensperger, K., Lindauer, M., & Hutter, F. (2018). Neural Networks for Predicting Algorithm Runtime Distributions. IJCAI 2018.
  3. Tuero, J. E., & Buro, M. (2021). Bayes DistNet: A Robust Neural Network for Algorithm Runtime Distribution Predictions. AAAI 2021, 35(13).
  4. Hollmann, N., Müller, S., Eggensperger, K., & Hutter, F. (2023). TabPFN: A Transformer That Solves Small Tabular Classification Problems in a Second. ICLR 2023.

About

Code for predicting algorithm runtime distributions with TabPFN

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages