"""
Batched LLM-Assisted Evolutionary Algorithm (LAEA-light)
This module implements LAEA-light, a drop-in variant of
:class:`~ddmtolab.Algorithms.STSO.LAEA.LAEA` that asks the language model about
a whole generation of offspring in one request instead of one request per
candidate.
The search itself is untouched -- same variable-width histogram EDA, same
quadratic local search, same one-real-evaluation-per-generation policy, and the
same two questions asked of the same in-context examples. Only the transport
changes: the regression prompt and the classification prompt each carry every
candidate of the current generation and come back with one answer per candidate.
Why this is worth a separate class: in LAEA about 97% of every prompt is the
in-context archive, and the serial path resends it once per candidate. At
``n_initial=50`` a generation therefore costs ``2 * 50 = 100`` requests and, at
D=10 with ``tao=50``, roughly 149,000 prompt tokens -- to buy a single real
function evaluation. Batching the whole generation brings that down to two
requests and about 3,500 prompt tokens.
``llm_batch_size`` caps how many candidates share a request, so a generation
costs ``2 * ceil(n_initial / llm_batch_size)`` requests. The default packs the
whole generation into one request per surrogate.
Notes on fidelity
-----------------
The two surrogates stay independent, as in LAEA: the regression answer never
appears in the classification prompt or the other way round. What does change is
that a candidate is now judged alongside its siblings rather than alone, so
predictions are not bit-identical to the serial ones and existing serial
``llm_cache`` files do not carry over. A response that cannot be parsed, or that
does not return exactly one answer per candidate, is retried and then falls back
per candidate exactly as the serial path does.
References
----------
[1] Hao, H., Zhang, X., & Zhou, A. (2024). Large Language Models as Surrogate Models in Evolutionary Algorithms: A Preliminary Study. arXiv preprint arXiv:2406.10675.
Notes
-----
Author: Jiangtao Shen
Email: j.shen5@exeter.ac.uk
Date: 2026.08.15
Version: 1.0
"""
from ddmtolab.Algorithms.STSO.LAEA import LAEA
from ddmtolab.Methods.Algo_Methods.algo_utils import get_algorithm_information
from ddmtolab.Methods.LLM_Methods.llm_surrogate import (
LLM_ClassificationBatch,
LLM_RegressionBatch,
)
[docs]
class LAEA_light(LAEA):
"""
LAEA with one LLM request per surrogate per generation instead of one
request per candidate.
"""
regression_cls = LLM_RegressionBatch
classification_cls = LLM_ClassificationBatch
# Declared literally rather than copied from LAEA: the GUI reads these two
# dicts out of the source with the AST, so a computed value would show up
# as an empty capability panel
algorithm_information = {
'n_tasks': '[1, K]',
'dims': 'unequal',
'objs': 'equal',
'n_objs': '1',
'cons': 'equal',
'n_cons': '0',
'expensive': 'True',
'knowledge_transfer': 'False',
'n_initial': 'unequal',
'max_nfes': 'unequal'
}
llm_information = {
'llm_role': 'surrogate',
'backend': 'openai_compatible',
'default_model': 'deepseek-chat',
'max_llm_calls': '[1, L]',
'prompt_version': 'laea_reg_v1 + laea_cla_v1',
'calls_per_generation': '2 * ceil(n_initial / llm_batch_size)',
'offline_replay': 'True',
'deterministic': 'cache_only'
}
@classmethod
def get_algorithm_information(cls, print_info=True):
return get_algorithm_information(cls, print_info)
[docs]
def __init__(self, problem, n_initial=None, max_nfes=None, tao=50, rate=0.3, pb=0.2, pc=0.2, m=15,
llm_batch_size=None,
llm_backend='replay', llm_model='deepseek-chat', llm_base_url='https://api.deepseek.com',
llm_api_key_env='DEEPSEEK_API_KEY', llm_temperature=0.0, llm_max_tokens=2048,
llm_max_retries=3, llm_parallel=1, llm_beta=3, llm_seed=42, max_llm_calls=None,
llm_price_prompt=0.0, llm_price_completion=0.0, llm_cache_path='',
strict_source=False, save_data=True, save_path='./Data',
name='LAEA-light', disable_tqdm=True):
"""
Initialize LAEA-light.
Parameters
----------
problem : MTOP
Multi-task optimization problem instance
n_initial : int or List[int], optional
Number of initial samples per task, also the population size and the
number of offspring per generation (default: 50)
max_nfes : int or List[int], optional
Maximum number of function evaluations per task (default: 300)
tao : int, optional
Number of best archive members shown to the LLM as examples (default: 50)
rate : float, optional
Quantile splitting good from bad solutions when labelling (default: 0.3)
pb : float, optional
Fraction of the population refined by local search (default: 0.2)
pc : float, optional
Per-variable probability of crossing an offspring with a
locally-refined solution (default: 0.2)
m : int, optional
Number of histogram bins per dimension in the VWH model (default: 15)
llm_batch_size : int, optional
Number of candidates packed into one request. ``None`` puts the whole
generation into one request per surrogate, which is the point of this
variant and costs two requests per real evaluation. A smaller value
trades that back for shorter, easier to parse responses and costs
``2 * ceil(n_initial / llm_batch_size)`` (default: None)
llm_backend : str, optional
'openai_compatible' to call a real endpoint, 'replay' to serve only
from the cache, or 'mock' for offline synthetic responses
(default: 'replay')
llm_model : str, optional
Model identifier (default: 'deepseek-chat')
llm_base_url : str, optional
Endpoint root, passed through verbatim (default: 'https://api.deepseek.com')
llm_api_key_env : str, optional
Environment variable holding the API key (default: 'DEEPSEEK_API_KEY')
llm_temperature : float, optional
Sampling temperature (default: 0.0)
llm_max_tokens : int, optional
Completion cap. A batched answer carries one entry per candidate, so
this is far larger than the serial default of 10 (default: 2048)
llm_max_retries : int, optional
Attempts per request before falling back (default: 3)
llm_parallel : int, optional
Worker threads issuing requests. Reduces wall-clock time, not the
number of requests (default: 1)
llm_beta : int, optional
Decimal places kept when rounding normalized features (default: 3)
llm_seed : int, optional
Seed forwarded to the endpoint and used by the fallback RNG (default: 42)
max_llm_calls : int, optional
Hard cap on requests; the run stops early when it is reached
(default: None)
llm_price_prompt : float, optional
USD per 1M prompt tokens, for the cost estimate (default: 0.0)
llm_price_completion : float, optional
USD per 1M completion tokens, for the cost estimate (default: 0.0)
llm_cache_path : str, optional
Cache file; defaults to ``<save_path>/llm_cache/<name>.jsonl``.
Serial LAEA caches are not compatible because the prompts differ
(default: '')
strict_source : bool, optional
Accept only the ``Value`` JSON key, reproducing the reference parser
(default: False)
save_data : bool, optional
Whether to save optimization data (default: True)
save_path : str, optional
Path to save results (default: './Data')
name : str, optional
Name for the experiment (default: 'LAEA-light')
disable_tqdm : bool, optional
Whether to disable progress bar (default: True)
"""
super().__init__(
problem, n_initial=n_initial, max_nfes=max_nfes, tao=tao, rate=rate,
pb=pb, pc=pc, m=m, llm_backend=llm_backend, llm_model=llm_model,
llm_base_url=llm_base_url, llm_api_key_env=llm_api_key_env,
llm_temperature=llm_temperature, llm_max_tokens=llm_max_tokens,
llm_max_retries=llm_max_retries, llm_parallel=llm_parallel,
llm_beta=llm_beta, llm_seed=llm_seed, max_llm_calls=max_llm_calls,
llm_price_prompt=llm_price_prompt, llm_price_completion=llm_price_completion,
llm_cache_path=llm_cache_path, strict_source=strict_source,
save_data=save_data, save_path=save_path, name=name,
disable_tqdm=disable_tqdm
)
# None means the whole generation in one request; n_initial is also the
# number of offspring, so it is the largest batch that can occur
self.llm_batch_size = int(llm_batch_size) if llm_batch_size else int(self.n_initial)