Code release accompanying the study "AdaEva: Accelerating LLM-Driven Algorithm Design with Adaptive Partial Evaluation".
LLM-driven automated algorithm design (LLM4AD) frameworks such as EoH, HiFo-Prompt and LLaMEA evaluate every generated heuristic
AdaEva is an instance-aware, adaptive evaluation layer that is dropped in between candidate generation and environmental selection:
-
Preserved: the LLM prompting engine, parent selection, code mutation/crossover operators and the per-instance simulator
$f(h;x)$ of each host framework remain unchanged. -
Intercepted: only the evaluation of the candidate pool
$\mathcal{U}^{(t)} = \Pi^{(t)} \cup \mathcal{O}^{(t)}$ . Instead of a full grid, AdaEva adaptively allocates per-candidate budgets$b_h^{(t,r)}$ over progressive steps$r$ and discards unpromising candidates early.
Two instantiations are provided:
| Variant | Mechanism | Code |
|---|---|---|
| AdaEva-R | Statistical iterated racing on a blocked instance prefix (Friedman omnibus + Conover or Nemenyi post-hoc test, or paired t-test) | src/racing/ |
| AdaEva-S | Cache-aware successive halving over geometrically growing instance budgets | src/sh/ |
These are compared against two static baselines, also included:
| Baseline | Allocation | Code |
|---|---|---|
| Fixed-$k$ (static partial evaluation) | every candidate on a fixed subset of |
src/fixed/ |
| Fixed-$K$ (full evaluation) | every candidate on the whole training pool | src/full/ |
Host frameworks × domains. EoH and HiFo-Prompt are run on three combinatorial problems: Online Bin Packing (OBP), TSP with guided local search (TSP-GLS) and Flow-Shop Scheduling with GLS (FSSP-GLS). LLaMEA is run on continuous black-box optimisation (BBOB, 24 noiseless functions in 5-D).
The code follows the notation of the paper:
| Symbol | Meaning | Where it appears in the code |
|---|---|---|
| Shared, ordered training instance pool |
--n-instances (print_instance_order
|
|
| Cost of heuristic |
host evaluator from packages/LLM4AD, packages/HiFo-Prompt or packages/LLaMEA
|
|
| Empirical mean cost on the prefix |
Config.costs_by_inst in src/utils/config.py
|
|
| Global budget of candidate–instance evaluations |
--budget-cap; defaults to pop_size × ref_max_generations × n_instances (the cost of Fixed-$K$ over the reference horizon) |
|
| Population size |
--pop-size (EoH/HiFo), --n-parents (LLaMEA) |
|
| Pool at generation |
combined in src/racing/base.py
|
|
| Contenders alive at racing step |
Config.alive |
|
| Survivors of the final racing step |
survivors returned by elitist_race
|
|
| Logged incumbent of generation |
select_incumbent in src/racing/base.py
|
Implemented in src/utils/race.py (elitist_race) and src/utils/tests.py. It follows the elitist race of irace.
-
Blocked prefix. All contenders are evaluated on the same instance sequence
$x_1, x_2, \dots$ (with a per-instance seed shared across candidates), so comparisons are paired and across-instance variance is blocked out. -
Carry-over and reuse. Costs are cached per
(candidate, instance)pair. Parents carried over from earlier generations reuse their stored costs; only fresh evaluations are charged against$B$ .--elitist-new-instancesnew instances are added to each race. -
Elimination test. After
--t-firstinstances, and then every--t-eachinstances:-
--test-type friedman(default): build the rank matrix on the shared prefix (average ranks for ties) and run the Friedman omnibus test at level--alpha. If it rejects, run a post-hoc comparison against the best-ranked contender:--posthoc-test-type conover(default; Conover as in irace) ornemenyi(critical-difference test). -
--test-type ttest: paired t-tests against the best contender. - Contenders that are significantly worse are eliminated.
-
-
Stopping. A race stops when the number of survivors reaches
$M$ , when the instance pool is exhausted, when the budget is exhausted, or after--elitist-limitconsecutive tests that eliminate nothing. -
Refill (optional,
--save-pop). If fewer than$M$ candidates survive, the population is topped up with the best eliminated candidates. Refilled candidates keepalive = False, so they are never reported as the incumbent.
Crashed candidates are rejected by default (--deal-with-crashed rejection) or given a large penalty cost (penalty). Candidates that exceed --eval-timeout receive --timeout-cost.
Implemented in _sh_schedule and the runners in src/sh/.
- From the pool size
$|\mathcal{U}^{(t)}|$ , population size$M$ and reduction factor$\eta$ (--sh-reduction-factor), the number of rounds is$\lceil \log_\eta(|\mathcal{U}^{(t)}|/M) \rceil$ . - Instance budgets grow geometrically from
--sh-min-instancesto$K$ . The last round always compares the survivors on all$K$ instances. - After each round, candidates are ranked by mean cost on the instances they share, and the top
$1/\eta$ fraction is promoted. - Evaluations are cached, as in AdaEva-R. A budget guard stops a round that would exceed
$B$ .
-
Search-time selection uses only the survivors
$\mathcal{V}^{(t)}$ and compares them on the instances they share. This prevents a candidate with a short, lucky prefix from entering$\Pi^{(t+1)}$ . -
Incumbent logging is passive: it costs no budget and does not feed back into the search. Each generation,
select_incumbentrecords$\hat h^{(t)}$ under two rules:-
mean_rank: the best-ranked survivor of the race. -
mean_cost: the lowest mean cost on the instances common to all eligible survivors.
-
-
Held-out validation. After the run, both incumbent trajectories are re-evaluated on a disjoint validation or test set (
valid_trajectory_mean_rank.json/valid_trajectory_mean_cost.json). The external benchmarks (TSPLIB, Taillard, held-out BBOB) are evaluated with the scripts insrc/analyses/.
AdaEva/
├── src/
│ ├── racing/ # AdaEva-R runners
│ │ ├── base.py # RacingBase: generation loop, budget accounting, incumbent logging
│ │ ├── eoh_{obp,tsp_gls,fssp_gls}.py
│ │ ├── hifo_{obp,tsp_gls,fssp_gls}.py
│ │ └── llamea_bbob.py
│ ├── sh/ # AdaEva-S runners (same task grid; hifo_common.py = shared HiFo glue)
│ ├── fixed/ # Fixed-k baseline (--K, --instance-mode fixed|random)
│ ├── full/ # Fixed-K baseline (full evaluation)
│ ├── init_pop/ # Fixed initial populations used with --fix-init-pop
│ ├── analyses/ # Held-out tests, validation re-evaluation, ranking & statistics analyses
│ └── utils/
│ ├── race.py # elitist_race (AdaEva-R core)
│ ├── tests.py # Friedman + Conover/Nemenyi, paired t-test
│ ├── config.py # Config / RaceState: per-(candidate, instance) cost cache
│ ├── ranking.py # rank matrices with tied mid-ranks
│ ├── llm.py # OpenAI-compatible / Gemini / Mistral / vLLM / Ollama clients + on-disk cache
│ ├── logger.py # run directories, stdout mirroring, optional W&B
│ ├── manuscript_log.py, race_log.py, hifo_candidate_log.py # structured JSONL logs
│ ├── fixed_init_pop.py, source_key.py, obp_utils.py
│ └── updated_tsp_eval/ # Adopted from EoH (examples/tsp_gls_numba): TSP data (TSP20/TSPAEL64/TSPLIB) + GLS evaluator
├── patches/ # Changes applied on top of the upstream packages (see below)
├── scripts/ # Cluster launch scripts (wrappers/, tasks/, submits/)
├── packages/ # Upstream repositories, cloned by the user (git-ignored)
├── .env.example
└── requirements.txt
Python 3.10 is used for all experiments.
conda create -n adaeva python=3.10 -y
conda activate adaeva
pip install -r requirements.txtpyconcorde (the exact TSP solver used for optimality gaps) is built from source and needs a C compiler. If it fails to build, the rest of the code still installs. Only the scripts that compute TSP optimality gaps need it.
AdaEva does not redistribute the host frameworks. Clone five public repositories from their original GitHub URLs into packages/ at the repository root. The code looks them up as <repo>/packages/<Name>, so keep the directory names unchanged.
| Directory | Original repository | Pinned commit | Used for |
|---|---|---|---|
packages/LLM4AD |
https://github.com/Optima-CityU/LLM4AD | e8d848f |
EoH method; OBP, TSP-GLS and FSSP-GLS tasks |
packages/HiFo-Prompt |
https://github.com/Challenger-XJTU/HiFo-Prompt | e64ce9e |
HiFo-Prompt method |
packages/LLaMEA |
https://github.com/XAI-liacs/LLaMEA | 1b86ae6 |
LLaMEA method; BBOB utilities |
packages/EoH |
https://github.com/FeiLiu36/EoH | 4725457 |
FSSP training instances |
packages/EoH-S |
https://github.com/FeiLiu36/EoH-S | 8310b05 |
Heterogeneous OBP dataset (analyses only) |
Step 2.1: clone and pin. Run from the AdaEva repository root:
mkdir -p packages && cd packages
git clone https://github.com/Optima-CityU/LLM4AD.git && git -C LLM4AD checkout e8d848fea67db0626a1669718685afbe338547b2
git clone https://github.com/Challenger-XJTU/HiFo-Prompt.git && git -C HiFo-Prompt checkout e64ce9edbfb4c8ebffd652b785b0c87261785586
git clone https://github.com/XAI-liacs/LLaMEA.git && git -C LLaMEA checkout 1b86ae67502c0fb22235175e1005fef9d752d39e
git clone https://github.com/FeiLiu36/EoH.git && git -C EoH checkout 472545785c936dcfc863d2bc0d6109cf23c7ce62
git clone https://github.com/FeiLiu36/EoH-S.git && git -C EoH-S checkout 8310b056ab0d4ea83d194f1d9e8c7873c0a3b892
cd ..Step 2.2: apply the AdaEva patches. Four of the repositories need small changes, which are shipped in patches/. Run from the repository root:
for p in LLM4AD HiFo-Prompt LLaMEA EoH-S; do
git -C packages/$p apply --whitespace=nowarn "$PWD/patches/$p.patch"
done| Patch | Change |
|---|---|
LLM4AD.patch |
Adds the fssp_gls task (Numba-accelerated GLS for permutation flow-shop, with the Taillard test sets). Makes tsp_gls_2O seedable and able to run without a wall-clock limit. Scales OBP Weibull item sizes with the bin capacity. |
HiFo-Prompt.patch |
Moves HiFo's internal constants (insight-pool size, credit weights, stagnation and progress thresholds, timeouts) into a new hifo_hp.py so the runners can set them. Fails loudly on an empty population. |
LLaMEA.patch |
Makes response parsing robust when no function or class name can be extracted. Fixes stdout redirection. |
EoH-S.patch |
Writes the heterogeneous OBP set under the file name used by the analyses. |
Step 2.3 (optional): generate the heterogeneous OBP dataset. Generation is seeded, so the result is deterministic. Only src/analyses/eval_obp_hetero.py needs this dataset:
(cd packages/EoH-S/datasets/obp && python generate_instances_weibull_training.py)
# -> packages/EoH-S/datasets/obp/dataset_200_2k_128_5_80.pklStep 2.4: expose the packages to Python. Nothing is pip install-ed from packages/. Put the sources on PYTHONPATH instead (run from the repository root, in every new shell):
export PYTHONPATH="$PWD/src:$PWD/packages/LLM4AD:$PWD/packages/LLaMEA:$PWD/packages/HiFo-Prompt/hifo/src:$PYTHONPATH"You should end up with:
AdaEva/
├── packages/
│ ├── EoH/
│ ├── EoH-S/
│ ├── HiFo-Prompt/
│ ├── LLaMEA/
│ └── LLM4AD/
├── patches/
└── src/
packages/ is git-ignored.
cp .env.example .env # then fill in the key(s) you need--llm-backend |
Required variables | Notes |
|---|---|---|
openrouter (default) |
OPENAI_API_KEY (or OPENROUTER_API_KEY) |
Any OpenAI-compatible endpoint via OPENAI_BASE_URL; handles reasoning models (gpt-5*, o-series) |
google |
GOOGLE_API_KEY |
Gemini via its OpenAI-compatible endpoint; GOOGLE_THINKING_BUDGET |
mistral |
MISTRAL_API_KEY |
Client-side throttling via MISTRAL_MIN_INTERVAL |
vllm |
VLLM_BASE_URL |
Self-hosted OpenAI-compatible server |
ollama |
OLLAMA_HOST (or --ollama-host) |
Local models |
The model is selected with --llm-model. The available --llm-backend choices differ slightly between runners; see --help.
All LLM responses are cached on disk under .llm_cache/ (override with --cache-root). The cache key is a SHA-256 hash of the messages, model and sampling parameters, plus a per-seed salt, so re-running an identical configuration costs no API calls and reproduces the same trajectory.
All runners share the same interface. With PYTHONPATH set as above, run each one from the repository root as python src/<method>/<framework>_<task>.py [options], where:
<method>is one ofracing,sh,fixedorfull;<framework>_<task>is one ofeoh_obp,eoh_tsp_gls,eoh_fssp_gls,hifo_obp,hifo_tsp_gls,hifo_fssp_glsorllamea_bbob.
# EoH on TSP-GLS: Friedman + Conover, budget B = 10 x 20 x 64
python src/racing/eoh_tsp_gls.py \
--pop-size 10 --ref-max-generations 20 --n-instances 64 --problem-size 100 \
--t-first 5 --t-each 1 --alpha 0.05 \
--test-type friedman --posthoc-test-type conover \
--elitist-limit 2 --fix-init-pop --seed 0 --num-cores 4 \
--llm-backend google --llm-model gemini-3.1-flash-lite
# LLaMEA on BBOB (24 functions, 5-D)
python src/racing/llamea_bbob.py \
--n-parents 10 --n-offspring 10 --ref-max-generations 20 --dim 5 \
--t-first 5 --t-each 2 --posthoc-test-type nemenyi \
--fix-init-pop --seed 0 --num-cores 4 \
--llm-backend vllm --llm-model mistralai/Devstral-Small-2-24B-Instruct-2512python src/sh/eoh_tsp_gls.py \
--pop-size 10 --ref-max-generations 20 --n-instances 64 \
--sh-min-instances 5 --sh-reduction-factor 1.33 \
--fix-init-pop --seed 0 --num-cores 4# Fixed-k: every candidate on k=4 fixed instances
python src/fixed/eoh_tsp_gls.py --pop-size 10 --max-generations 20 --K 4 --instance-mode fixed --fix-init-pop
# Fixed-K: every candidate on the full training pool
python src/full/eoh_tsp_gls.py --pop-size 10 --max-generations 20 --n-instances 64| Option | Default | Description |
|---|---|---|
--pop-size / --n-parents
|
10 | Population size |
--n-instances |
runner-specific | Training pool size |
--ref-max-generations |
20 | Reference horizon used to set the default budget |
--budget-cap |
pop × ref_gens × K |
Evaluation budget -1 disables it) |
--max-generations |
runner-specific | Generation cap (-1 disables it; the run then stops on |
--t-first / --t-each
|
5 / 1–2 | Instances before the first test / between tests |
--alpha |
0.05 | Significance level |
--test-type |
friedman |
friedman or ttest
|
--posthoc-test-type |
conover |
conover or nemenyi
|
--elitist-limit |
2 (12 for BBOB) | Stop a race after this many tests that eliminate nothing |
--no-elitist |
— | Disable reuse of parents' costs across generations |
--save-pop |
off | Refill the population with eliminated candidates when survivors < |
--deterministic |
off | Treat evaluation as deterministic; by default each instance is paired with a seed that is shared by all candidates (irace-style stochastic tasks) |
--sh-min-instances / --sh-reduction-factor
|
5 / 1.33 | AdaEva-S first-round budget and |
--K, --instance-mode
|
1, fixed
|
Fixed-$k$ subset size and fixed vs. resampled subset |
--fix-init-pop |
off | Start from src/init_pop/<framework>_<task>.json
|
--seed |
0 | Run seed (instances, LLM cache salt) |
--num-cores / --num-threads
|
4 | Parallel evaluation workers / LLM sampler threads |
--use-wandb |
off | Also log trajectories to Weights & Biases |
Run python src/<method>/<runner>.py --help for the full list, including the HiFo-Prompt constants (--pool-capacity, --credit-best, …) and the LLaMEA options (--parent-selection, --instance-pool-mode, --prompt-mode, …).
Each run writes to .logs/<label>/<timestamp>_<seed>_<tag>/:
| File | Content |
|---|---|
trajectory.json, heuristics.json |
Per-generation best/incumbent and the code of every heuristic |
race_log.jsonl, race_step_trace.jsonl, race_summary.jsonl |
Race decisions per step and per generation (AdaEva-R/S) |
candidate_log.jsonl, all_candidates.jsonl |
Candidate lifecycle, genealogy and status (survived / eliminated / crashed / timeout) |
population_diversity_log.jsonl, fitness_reliability_log.jsonl |
Diversity and partial-vs-full score diagnostics |
valid_trajectory_{mean_rank,mean_cost}.json |
Held-out validation of the logged incumbents (valid_trajectory.json for the Fixed baselines) |
llm_prompts.jsonl, timings.json, run_meta.json, terminal.txt |
LLM I/O and token usage, wall-clock timings, configuration, console log |
Scripts in src/analyses/ operate on a run directory (--exp <run_dir>):
# Held-out benchmarks for the final or per-generation incumbent
python src/analyses/test_tsplib.py --exp .logs/race/tsp_gls/<run> --has-incumbent --incumbent-mode mean_rank
python src/analyses/test_fssp_taillard.py --exp .logs/race/fssp_gls/<run> --has-incumbent
python src/analyses/test_bbob.py --exp .logs/racing_llamea_bbob/<run> --has-incumbent
python src/analyses/test_obp_slice.py --exp .logs/race/obp/<run> --has-incumbenttest_*.py evaluates the final heuristic; test_*_slice.py evaluates checkpoints along the run.
MIT. See LICENSE. The repositories cloned into packages/ are distributed under their own licenses.
src/utils/updated_tsp_eval/ is adopted from the EoH repository, folder examples/tsp_gls_numba (MIT License, © Fei Liu; Liu et al., Evolution of Heuristics: Towards Efficient Automatic Algorithm Design Using Large Language Model, ICML 2024).
- Unchanged: all data files: the TSP20 and TSPAEL64 instance sets and the TSPLIB instances with their optimal tours.
- Adapted for AdaEva:
prob.py,evaluation/evaluation.py,evaluation/heuristic.pyandevaluation/runEval.py. - Added:
evaluation/run_eval_eoh.py.
