Summary
StandardPipeline.show_details (default is_show_details=True) computes fitness statistics with
Python's builtin max()/min() over the fitness tensor
(src/evogp/pipeline/standard.py lines 96–97, commit 5538d75):
max_f, min_f, mean_f, std_f = (
max(valid_fitness), # Python builtin -> element-by-element tensor iteration
min(valid_fitness), # Python builtin
torch.mean(valid_fitness),
torch.std(valid_fitness),
)
The builtins iterate the tensor element-by-element in the Python interpreter (one __getitem__
plus comparison per element), so this logging step costs O(pop_size) interpreter time every
generation, and at large populations it dominates the entire evolutionary loop.
Measurements (RTX 5070 Ti, torch 2.9.0+cu130, evogp @ 5538d75)
Microbenchmark on a 1M-element fitness tensor:
import torch, time
t = -torch.rand(1_000_000) * 100
t0 = time.perf_counter(); m = max(t); print(time.perf_counter() - t0) # 1.48 s
t0 = time.perf_counter(); m = torch.max(t); print(time.perf_counter() - t0) # 0.0003 s
Pipeline-level impact (SR, pop 1M, 100 generations, D=64, TournamentSelection + DefaultCrossover
|
time / generation |
is_show_details=True (default) |
~2.5 s |
is_show_details=False |
~0.09 s |
i.e. with the default setting, ~96% of wall-clock at this population size is the two builtin
calls in the logging path — not the (genuinely fast) evolutionary pipeline. At pop 10k the
logging is still ~90% of run time (~2.6 s vs ~0.28 s per 100 generations).
This matters beyond aesthetics: anyone timing StandardPipeline.run() at large populations with
the default settings (e.g. for benchmarking or framework comparisons) will measure the logging,
not EvoGP — we hit exactly this while running a comparison study and initially mis-measured
EvoGP's whole-run performance by ~25× before tracing it here.
Suggested fix (one line each)
max_f, min_f, mean_f, std_f = (
torch.max(valid_fitness),
torch.min(valid_fitness),
torch.mean(valid_fitness),
torch.std(valid_fitness),
)
(len(valid_fitness) in the print is fine; optionally the whole stats block could stay on GPU
and transfer scalars only.)
Environment
- evogp installed from source at commit
5538d75 (pip install --no-build-isolation .)
- PyTorch 2.9.0+cu130, CUDA 13.1, NVIDIA RTX 5070 Ti (sm_120), Linux
I will open a PR :) — and thanks for the framework; the pipeline itself
benchmarks impressively once the logging is bypassed.
Summary
StandardPipeline.show_details(defaultis_show_details=True) computes fitness statistics withPython's builtin
max()/min()over the fitness tensor(
src/evogp/pipeline/standard.pylines 96–97, commit5538d75):The builtins iterate the tensor element-by-element in the Python interpreter (one
__getitem__plus comparison per element), so this logging step costs O(pop_size) interpreter time every
generation, and at large populations it dominates the entire evolutionary loop.
Measurements (RTX 5070 Ti, torch 2.9.0+cu130, evogp @
5538d75)Microbenchmark on a 1M-element fitness tensor:
Pipeline-level impact (SR, pop 1M, 100 generations, D=64, TournamentSelection + DefaultCrossover
is_show_details=True(default)is_show_details=Falsei.e. with the default setting, ~96% of wall-clock at this population size is the two builtin
calls in the logging path — not the (genuinely fast) evolutionary pipeline. At pop 10k the
logging is still ~90% of run time (~2.6 s vs ~0.28 s per 100 generations).
This matters beyond aesthetics: anyone timing
StandardPipeline.run()at large populations withthe default settings (e.g. for benchmarking or framework comparisons) will measure the logging,
not EvoGP — we hit exactly this while running a comparison study and initially mis-measured
EvoGP's whole-run performance by ~25× before tracing it here.
Suggested fix (one line each)
(
len(valid_fitness)in the print is fine; optionally the whole stats block could stay on GPUand transfer scalars only.)
Environment
5538d75(pip install --no-build-isolation .)I will open a PR :) — and thanks for the framework; the pipeline itself
benchmarks impressively once the logging is bypassed.