perf(database): stop recomputing per-iteration values in the controller path - #477
Open
dexhunter wants to merge 1 commit into
Open
Conversation
…er path _fast_code_diversity re-scans both program strings on every call, so the 20 reference-set strings are re-scanned once per comparison; Program.to_dict goes through dataclasses.asdict, which re-derives the field list and deep-copies every node once per program per iteration; _cache_diversity_value picks its eviction victim with an O(cache_size) min() over timestamps; and _update_feature_stats rebuilds its 1000-element window with a slice on every call. Memoize the derived code shape (length, newline count, character set) per code string, build the program dict from a field tuple resolved once with fast paths for the plain values a Program holds, evict by dict insertion order, and trim the feature-stats window in place. Controller CPU per evolution iteration at the shipped defaults (population_size=1000, num_islands=5) goes from 31.74/30.05 ms to 5.13/5.18 ms, about 25 ms per iteration. No behaviour change: to_dict output stays identical to dataclasses.asdict including independent copies of nested containers, and island membership, the archive, both feature maps, the feature statistics and seeded parent/inspiration sampling are unchanged. Co-Authored-By: Aiden <aiden@weco.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Four operations on the controller's per-iteration path in
openevolve/database.pyrecompute values that are fully determined by data the database already holds. This
caches or restructures them. Behaviour is unchanged: same snapshot contents, same
MAP-Elites placement, same sampling, same island, archive and feature-map state.
_fast_code_diversityre-scans both program strings on every call._get_cached_diversity()compares one program against the whole reference set, so oneach cache miss the 20 reference strings are re-scanned once per comparison and the
program's own string is re-scanned once per reference entry. Each scan is
len(code),code.count("\n")andset(code), all O(len(code)) and all functions ofthe string alone. A new
_code_shape()memoizes the three derived values per codestring, bounded at 2048 entries with the same insertion-order eviction the diversity
cache already uses.
Program.to_dict()callsdataclasses.asdict(self).asdict()re-derives the field list on every call and walks every node through_asdict_inner()andcopy.deepcopy().to_dict()now walks a module-level fieldtuple resolved once and uses
_copy_field_value(), which has fast paths for theplain
str/int/float/dict/listvalues aProgramactually holds and fallsback to
asdict()/copy.deepcopy()for anything else. The returned value isidentical, including independent copies of the nested containers, so a caller mutating
the result still cannot reach back into the database.
_cache_diversity_value()scans the whole cache to pick the eviction victim.min(self.diversity_cache.items(), key=…["timestamp"])is O(cache_size) on everyinsert once the 1000-entry cache is full. Every insert carries a later timestamp than
the one before it, and dicts preserve insertion order, so
next(iter(...))selectsthe same entry without the scan.
_update_feature_stats()rebuilds its 1000-element window on every call.stats["values"] = stats["values"][-1000:]allocates and copies a fresh1000-element list each time the window is full. The list is trimmed one element at a
time, so
del values[0]produces the same window in place.Measurement
Controller-side CPU per evolution iteration, at the shipped defaults
(
population_size=1000,num_islands=5), over the production pathsample_from_island→_create_database_snapshot→add→increment_island_generation→should_migrate:main@411fb59The figure is the difference in
wait4()rusage between two child processes that differonly in how many iterations they run, so the measuring parent produces the number, not
the code under test. Each entry is the median of three repetitions over three
independently randomized 1000-program populations, holding an exclusive host CPU lock for
the timed section.
Attribution before the change, from
cProfileon the same path:_fast_code_diversity46% of the measured region (about 370 calls per iteration),
Program.to_dict→asdict37% (one call per program per iteration over the whole steady-state population),
_cache_diversity_value5.6%.What this is and is not. This is controller-process CPU, not user-visible latency.
Every iteration also runs an LLM generation and a program evaluation, and those dominate
wall time by orders of magnitude. I am not claiming an end-to-end speedup. What it
removes is controller CPU currently spent recomputing values the database already knows,
at a call frequency of one per iteration with
max_iterationsdefaulting to 10,000. Theabsolute figure scales roughly with
population_size, so it is smaller on small runs.Correctness
python -m unittest discover tests(437 cases) passes with the same 7 pre-existingloader errors as unmodified
main. Thetests/integration/*modules andtest_utilsfail to import in a plain unit-test environment both before and after this change.
to_dict()output againstdataclasses.asdict()over a 1000-programpopulation and the two are identical, and I checked that the result does not alias any
database-owned dict or list.
and 20 seeded
sample_from_islandparent/inspiration selections against an unmodifiedcopy of the repository at the same commit, on the same randomized fixture. They match.
black --line-length 100leaves the changed file unchanged.One note, because it cost me several attempts: a cache that a memoizing helper depends on
has to be bound in the first statements of
ProgramDatabase.__init__.__init__callsself.load(), which reacheslog_island_status()→get_island_stats()→_calculate_island_diversity()→_fast_code_diversity()before the rest of__init__has run. Binding it next to the other diversity-cache attributes raises
AttributeErrorand fails six of the repository's own cases. The size bound is a class attribute for the
same reason.
Deliberately left out
Each of these measured faster and is not here:
Programinstances, and cachingto_dict()outputkeyed by
program.id. Both are faster, and both memoize values derived from mutablestate (
program.metrics, and the program itself) with no invalidation point and nosize bound. A stale entry would silently change MAP-Elites placement, and an unbounded
one grows for the life of the process.
_enforce_population_limit/_sample_inspirations. Both appear inthe profile, but every rewrite I tried changed island or MAP-Elites behaviour and
failed the repository's own tests. That is a separate change with a separate
correctness argument.
OrderedDict.move_to_end).Faster to evict, but it changes which entries survive. The current cache is
insertion-ordered and this PR keeps that exactly.
changes the diversity values and therefore the feature binning, which is a product
decision rather than an optimization.
The direction came from an automated optimization search over
openevolve/database.pyscored on the metric above. The search record is at
https://dashboard.weco.ai/share/1kKHOUfZCqxUF6WGTepRtSouUUopojKt (10 scored candidates
over 21 attempts, from a 32.20 ms measured baseline down to 3.90 ms). I rebuilt this diff
by hand against current
mainand re-measured it, and I offer the link as a record of thesearch, not as authorship of this patch.
The search's best candidate is not what this PR proposes. It reaches 3.90 ms partly by
caching derived code statistics against
program.idin an unbounded dict, which is thesame un-invalidated-cache problem described under "Deliberately left out". This branch
keeps only the changes that are safe under mutation and bounded in memory.