Parallelise outer search population evaluation (homemaker-py-5l6)
Add n_workers parameter to driver.search(). When n_workers > 1, a ProcessPoolExecutor evaluates the bootstrap batch and main-loop children in parallel, giving near-linear speedup with core count. The geometry module-level cache is cleared in each worker after fork to prevent stale id-keyed entries. Serial behaviour (n_workers=1, default) is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
304d514573
commit
7ee51caf62
3 changed files with 112 additions and 38 deletions
|
|
@ -19,7 +19,7 @@
|
|||
{"id":"homemaker-py-d0s","title":"Experiment: inner-loop optimiser bake-off at equal oracle budgets","description":"DESIGN.md §7 Phase 1, §8.3. DOF is only ~rooms-1 (6–7 on corpus). Compare Nelder-Mead vs CMA-ES vs batched multi-start pattern search at equal oracle-call budgets, measuring fitness gained per oracle call and wall-clock (batch-friendliness matters — §4.6). Measure, don't commit blind.","acceptance_criteria":"Table of fitness-per-budget across \u003e=3 candidates; one optimiser chosen and recorded in DESIGN.md","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:59Z","created_by":"Bruno Postle","updated_at":"2026-06-13T08:48:13Z","started_at":"2026-06-12T21:22:15Z","closed_at":"2026-06-13T08:48:13Z","close_reason":"Bake-off complete: CMA-ES confirmed as Phase 1/2 optimiser. NM wins quality per eval but sequential architecture incompatible with batching (§4.6). Compass stalls on narrow valleys. Results in DESIGN.md §8.3 and experiments/bakeoff_innerloop.*","dependencies":[{"issue_id":"homemaker-py-d0s","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:35Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-9t6","title":"Package install: pyproject.toml with entry points","description":"The project currently requires PYTHONPATH=/home/bruno/src/homemaker-py/src and is run via 'python3 experiments/...'. There is no installable package. Add a pyproject.toml with: package discovery for src/homemaker/, a [project.scripts] entry point for homemaker-evolve (homemaker-py-2wc), and minimal metadata. After 'pip install -e .' the tool should be on PATH and importable without PYTHONPATH. Keep the existing pyproject.toml if one exists and extend it.","acceptance_criteria":"'pip install -e .' succeeds; 'homemaker-evolve --help' works from any directory; 'import homemaker' works without PYTHONPATH","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-13T21:52:35Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:52:35Z","dependencies":[{"issue_id":"homemaker-py-9t6","depends_on_id":"homemaker-py-2wc","type":"blocks","created_at":"2026-06-13T22:52:41Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-gug","title":"Test suite","description":"There are no automated tests. Validation has been done entirely through experiment scripts and the 35-file corpus parity check (homemaker-py-uxz). This is acceptable during exploration but fragile as the codebase grows. Need pytest-based unit tests covering: geometry port correctness (vs known values, not just vs oracle), fitness term correctness (size/width/proportion/adjacency/access/crinkliness/stair terms individually), genome operators (mutations preserve tree invariants), inner loop (convergence on known landscape), and a fast corpus smoke test (subset of the 35 files, score within tolerance). The corpus parity experiment can be the integration test baseline.","acceptance_criteria":"pytest runs clean; geometry, fitness terms, operators, and inner loop each have unit tests; corpus smoke test covers at least 5 files","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T21:52:31Z","created_by":"Bruno Postle","updated_at":"2026-06-13T22:51:04Z","started_at":"2026-06-13T22:40:56Z","closed_at":"2026-06-13T22:51:04Z","close_reason":"Added test_geometry.py (26 tests) and test_fitness.py (35 tests); full suite now 175 tests, all passing","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-5l6","title":"Parallelise outer search population evaluation","description":"The outer memetic search evaluates topologies sequentially. Each eval runs the inner loop (CMA-ES) to convergence — independent across population members. Native fitness is pure Python with no shared mutable state, so population evaluation is embarrassingly parallel. multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor over the child generation batch would give near-linear speedup with population size. At 71.8 evals/s single-threaded on a seeded programme-house run, parallelisation across available cores would proportionally increase the effective budget within the same wall-clock time.","acceptance_criteria":"Population generation parallelised; throughput scales with core count; verified correct (same result distribution as serial)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-13T21:52:29Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:52:29Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-5l6","title":"Parallelise outer search population evaluation","description":"The outer memetic search evaluates topologies sequentially. Each eval runs the inner loop (CMA-ES) to convergence — independent across population members. Native fitness is pure Python with no shared mutable state, so population evaluation is embarrassingly parallel. multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor over the child generation batch would give near-linear speedup with population size. At 71.8 evals/s single-threaded on a seeded programme-house run, parallelisation across available cores would proportionally increase the effective budget within the same wall-clock time.","acceptance_criteria":"Population generation parallelised; throughput scales with core count; verified correct (same result distribution as serial)","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T21:52:29Z","created_by":"Bruno Postle","updated_at":"2026-06-14T05:55:16Z","started_at":"2026-06-14T05:37:13Z","closed_at":"2026-06-14T05:55:16Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-d6d","title":"Revisit Nelder-Mead for inner loop (post-oracle)","description":"The Phase 1 bakeoff (homemaker-py-d0s) chose CMA-ES over Nelder-Mead because CMA batches oracle calls (18 vs 200 per topology) — critical when oracle cost is 1 s/dom. That constraint is gone: native fitness evaluates at 71.8 evals/s with no batching penalty. The bakeoff showed NM wins quality per eval by +15% at budget 200 (x1.56 vs x1.41 gain). NM is also simpler, has no hyperparameters, and is inherently sequential which matches the inner loop's single-topology use. Re-run the bakeoff with native fitness; if NM still wins, swap it in. Also evaluate gradient-based optimisation (autograd through the native fitness functions) as a potential further improvement.","acceptance_criteria":"Bakeoff re-run with native fitness; inner loop updated if NM or gradient method outperforms CMA-ES; gain improvement documented","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-13T21:52:27Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:52:27Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-2wc","title":"CLI tool: homemaker-evolve (equivalent to urb-evolve.pl)","description":"Wrap the existing memetic search driver as a proper command-line tool, analogous to urb-evolve.pl. The tool should: accept a programme directory and optional seed .dom file as positional args; honour env vars for budget/population (MAX_ITERATIONS, MAX_POP or equivalents); write the best .dom found to the programme directory (or stdout); print progress to stderr; handle SIGINT/SIGTERM gracefully (write best-so-far and exit cleanly). The bulk of the logic already exists in driver.py and experiments/run_search_scaled.py — this is a thin wrapper that makes the search usable from the shell and composable with other tools. Install as bin/homemaker-evolve or src/homemaker/bin/homemaker-evolve.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-13T21:47:55Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:47:55Z","dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||
{"id":"homemaker-py-8fe","title":"Fix Urb programme width default (upstream of homemaker-py-can fix)","description":"The native fitness fix in homemaker-py-can derives a sane width from sqrt(size/proportion) when a programme space has no explicit width. The same bug exists upstream in Perl Urb: Fitness/Base.pm and ProgrammeDriven.pm fall back to width_inside [4.0, 1.0] for any programme space without an explicit width key. Fix the Perl oracle to match the native behaviour (same sqrt(size/proportion) formula).","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T21:18:19Z","created_by":"Bruno Postle","updated_at":"2026-06-13T22:14:17Z","started_at":"2026-06-13T21:43:33Z","closed_at":"2026-06-13T22:14:17Z","close_reason":"Fixed: get_space_params now derives width from sqrt(size/proportion) when no explicit width key is present. 34/36 corpus files score higher with the fix; all 111 tests pass after rescoring with URB_NO_OCCLUSION=1.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
|
@ -27,9 +27,9 @@
|
|||
{"id":"homemaker-py-yg5","title":"Penalty reshaping: replace 0.5^n while preserving inner-loop protection","description":"DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.","acceptance_criteria":"Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:00Z","created_by":"Bruno Postle","updated_at":"2026-06-11T23:39:00Z","dependencies":[{"issue_id":"homemaker-py-yg5","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-9gp","title":"Canonical slicing encoding (normalized Polish expression) + shape feasibility","description":"DESIGN.md §5.5, §7 Phase 5. Representation upgrade once core lands: normalized Polish expression / skewed slicing tree (Wong–Liu) for redundancy-free, high-locality topology moves (M1/M2/M3); bottom-up shape-feasibility checks to prune infeasible topologies before the inner loop. Goal: scale to larger programmes. Excluded representations stay excluded (§2): no sequence-pair/B*-tree (non-slicing).","acceptance_criteria":"Encoding round-trips with the genome; M1/M2/M3 moves implemented; measured search improvement on a larger-than-house programme","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:02Z","created_by":"Bruno Postle","updated_at":"2026-06-11T23:39:02Z","dependencies":[{"issue_id":"homemaker-py-9gp","depends_on_id":"homemaker-py-ccw","type":"blocks","created_at":"2026-06-12T00:39:48Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"id":"homemaker-py-2g5","title":"Rebuild occlusion/daylight/sun subsystem in Python (post-Phase-5, after optimisation fully native)","description":"DESIGN.md §6 port scope — a whole subsystem, not a term. quality_daylight (Leaf.pm:281-296) needs Urb::Misc::Sun + Urb::Field::Occlusion (+CIESky); quality_uncrinkliness also takes the occlusion object. Indoor spaces return 1 for daylight; cost is outdoor spaces + crinkliness. Port Sun_horizontal (262980-minute normalisation) and the occlusion wall set from Dom-\u003eWalls.","acceptance_criteria":"Daylight and crinkliness factors match Perl (float tolerance) across the corpus, including multi-storey cases","notes":"Re-scoped 2026-06-12: occlusion disabled in the Urb oracle instead of ported (see homemaker-py-gp2). Native fitness ships with simple crinkliness (illumination factor = 1, in homemaker-py-gnw). This issue is now the eventual Python occlusion rebuild, only after optimisation works entirely in Python. Restores outdoor-daylight and shaded-wall selection pressure.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:25Z","created_by":"Bruno Postle","updated_at":"2026-06-12T07:27:48Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
||||
{"_type":"memory","key":"strategy-decision-2026-06-12-bruno-occlusion-daylight","value":"Strategy decision 2026-06-12 (Bruno): occlusion/daylight is ORTHOGONAL to building a scalable optimiser. Disable it in Urb (env flag, homemaker-py-gp2) rather than port it; native fitness uses simple crinkliness (illumination factor = 1); rebuild occlusion in Python only after optimisation is fully native (homemaker-py-2g5, now P4). Consequence: all scores change when the flag flips — re-baseline corpus/.score, DESIGN \\$4.5 gains, gate bars at one clean boundary AFTER homemaker-py-1p0 closes; Phase-2 urb-evolve benchmark must run with the same flag."}
|
||||
{"_type":"memory","key":"urb-oracle-nondeterminism-urb-fitness-pl-output-varies","value":"Urb oracle nondeterminism: urb-fitness.pl output varies run-to-run from Perl hash-order randomisation — .fails line ORDER shuffles (compare sorted, use oracle.Score.fail_lines) and the score float can flip by ~1 ULP (compare with math.isclose rel_tol=1e-12, never ==). Not a batching artifact; affects single runs too. Matters for the Phase 3 native-fitness parity gate (homemaker-py-uxz)."}
|
||||
{"_type":"memory","key":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."}
|
||||
{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
||||
{"_type":"memory","key":"urb-oracle-nondeterminism-urb-fitness-pl-output-varies","value":"Urb oracle nondeterminism: urb-fitness.pl output varies run-to-run from Perl hash-order randomisation — .fails line ORDER shuffles (compare sorted, use oracle.Score.fail_lines) and the score float can flip by ~1 ULP (compare with math.isclose rel_tol=1e-12, never ==). Not a batching artifact; affects single runs too. Matters for the Phase 3 native-fitness parity gate (homemaker-py-uxz)."}
|
||||
{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-py PYTHONPATH: set PYTHONPATH=/home/bruno/src/homemaker-py/src or use 'python -m pytest' from the project root (which reads pyproject.toml and adds src/ automatically). Never try 'pip show' or 'pip install' — it's not installed as a package."}
|
||||
{"_type":"memory","key":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ applying divide mutations until each topology has approximately the programme
|
|||
room count, then evaluates all pop_size individuals before the memetic loop
|
||||
begins. This crosses the zero-feasibility region that single-seed chaining
|
||||
cannot escape.
|
||||
|
||||
Parallelism (homemaker-py-5l6): ``n_workers > 1`` evaluates a batch of
|
||||
children per iteration using ``concurrent.futures.ProcessPoolExecutor``.
|
||||
Each worker is independent (NativeEvaluator has no shared mutable state).
|
||||
The geometry module-level cache is cleared in each worker after fork to
|
||||
prevent stale id-keyed entries inherited from the parent process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -39,6 +45,17 @@ _CHILD_INNER_KW = {"sigmas": (0.05,)}
|
|||
_MUTATION_WEIGHTS = {"level_add": 0.2, "level_delete": 0.2}
|
||||
|
||||
|
||||
def _worker_init() -> None:
|
||||
"""Clear the geometry cache in each forked worker process.
|
||||
|
||||
geometry._cache is keyed by id(node) (Python memory address). After
|
||||
fork the inherited cache holds parent-process ids that could collide
|
||||
with freshly allocated nodes in the worker, producing wrong hits.
|
||||
"""
|
||||
from . import geometry
|
||||
geometry.clear_cache()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Individual:
|
||||
root: dom.Node
|
||||
|
|
@ -101,6 +118,7 @@ def search(
|
|||
inner_kw: dict | None = None,
|
||||
urb_root=None,
|
||||
log=None,
|
||||
n_workers: int = 1,
|
||||
) -> SearchResult:
|
||||
"""Run the memetic loop from ``seed_root`` until ``budget`` oracle
|
||||
evaluations are consumed. Returns the best individual found; its ``root``
|
||||
|
|
@ -111,6 +129,13 @@ def search(
|
|||
random topologies (each with approximately ``bootstrap_n_leaves`` leaves)
|
||||
before the memetic loop starts. Pass ``bootstrap=False`` to force the
|
||||
legacy single-seed path (appropriate for warm starts from existing designs).
|
||||
|
||||
``n_workers=1`` (default) runs serially; ``n_workers > 1`` evaluates
|
||||
children in parallel using ``ProcessPoolExecutor``. The bootstrap batch
|
||||
is fully parallel; the main loop generates ``n_workers`` children per
|
||||
iteration from the current population snapshot and evaluates them in
|
||||
parallel. Results are admitted in completion order (fastest first), so
|
||||
later children in each batch see an already-updated population.
|
||||
"""
|
||||
from .oracle import DEFAULT_URB_ROOT
|
||||
|
||||
|
|
@ -157,43 +182,80 @@ def search(
|
|||
pop[worst] = ind
|
||||
|
||||
pop: list[Individual] = []
|
||||
if do_bootstrap:
|
||||
# Bootstrap: diverse initial population from random topologies.
|
||||
# Each individual is a cold start, so use the exploratory sigma
|
||||
# schedule (inner_kw={} → cma_search defaults: sigmas=(0.05, 0.15)).
|
||||
# Leaf count varied ±1 around the target to increase structural diversity.
|
||||
n_target = bootstrap_n_leaves or max(len(reqs), 3)
|
||||
for i in range(pop_size):
|
||||
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
||||
topo = random_topology(seed_root, n, rng, types)
|
||||
ind, used = _evaluate(topo, programme_dir, urb_root,
|
||||
x0=None, budget=child_budget,
|
||||
inner_kw={}, lineage=f"bootstrap/{i}")
|
||||
n_evals += used
|
||||
admit(ind, pop)
|
||||
else:
|
||||
seed_ind, used = _evaluate(copy.deepcopy(seed_root), programme_dir, urb_root,
|
||||
x0=None, budget=seed_budget,
|
||||
inner_kw={}, lineage="seed")
|
||||
n_evals += used
|
||||
admit(seed_ind, pop)
|
||||
|
||||
while n_evals < budget:
|
||||
if len(pop) >= 2 and rng.random() < p_crossover:
|
||||
a, b = _tournament(pop, rng), _tournament(pop, rng)
|
||||
child_root, _, desc = operators.crossover(a.root, b.root, rng)
|
||||
ratios = {**b.ratios, **a.ratios} # primary parent wins
|
||||
# Set up optional process pool for parallel child evaluation.
|
||||
_pool = None
|
||||
if n_workers > 1:
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
_pool = ProcessPoolExecutor(max_workers=n_workers, initializer=_worker_init)
|
||||
|
||||
def _run_batch(
|
||||
tasks: list[tuple], # (root, x0, budget_, inner_kw_, lineage)
|
||||
) -> None:
|
||||
"""Evaluate a batch of tasks and admit results; parallel when _pool set."""
|
||||
nonlocal n_evals
|
||||
full = [
|
||||
(root, programme_dir, urb_root, x0, budget_, kw_, lin)
|
||||
for root, x0, budget_, kw_, lin in tasks
|
||||
]
|
||||
if _pool is not None:
|
||||
from concurrent.futures import as_completed
|
||||
futs = [_pool.submit(_evaluate, *t) for t in full]
|
||||
for f in as_completed(futs):
|
||||
ind, used = f.result()
|
||||
n_evals += used
|
||||
admit(ind, pop)
|
||||
else:
|
||||
parent = _tournament(pop, rng)
|
||||
child_root, desc = operators.mutate(parent.root, rng, types,
|
||||
weights=_MUTATION_WEIGHTS)
|
||||
ratios = parent.ratios
|
||||
x0 = innerloop.warm_x0(child_root, ratios)
|
||||
child, used = _evaluate(child_root, programme_dir, urb_root,
|
||||
x0=x0, budget=child_budget,
|
||||
inner_kw=inner_kw, lineage=desc)
|
||||
n_evals += used
|
||||
admit(child, pop)
|
||||
for t in full:
|
||||
ind, used = _evaluate(*t)
|
||||
n_evals += used
|
||||
admit(ind, pop)
|
||||
|
||||
try:
|
||||
if do_bootstrap:
|
||||
# Bootstrap: diverse initial population from random topologies.
|
||||
# Each individual is a cold start, so use the exploratory sigma
|
||||
# schedule (inner_kw={} → cma_search defaults: sigmas=(0.05, 0.15)).
|
||||
# Leaf count varied ±1 around the target to increase structural diversity.
|
||||
n_target = bootstrap_n_leaves or max(len(reqs), 3)
|
||||
tasks = []
|
||||
for i in range(pop_size):
|
||||
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
||||
topo = random_topology(seed_root, n, rng, types)
|
||||
tasks.append((topo, None, child_budget, {}, f"bootstrap/{i}"))
|
||||
_run_batch(tasks)
|
||||
else:
|
||||
seed_ind, used = _evaluate(copy.deepcopy(seed_root), programme_dir, urb_root,
|
||||
x0=None, budget=seed_budget,
|
||||
inner_kw={}, lineage="seed")
|
||||
n_evals += used
|
||||
admit(seed_ind, pop)
|
||||
|
||||
while n_evals < budget:
|
||||
# How many children to generate this iteration: n_workers in parallel,
|
||||
# but cap at what the remaining budget can afford (ceiling division).
|
||||
batch_n = (
|
||||
min(n_workers,
|
||||
max(1, (budget - n_evals + child_budget - 1) // child_budget))
|
||||
if _pool is not None else 1
|
||||
)
|
||||
tasks = []
|
||||
for _ in range(batch_n):
|
||||
if len(pop) >= 2 and rng.random() < p_crossover:
|
||||
a, b = _tournament(pop, rng), _tournament(pop, rng)
|
||||
child_root, _, desc = operators.crossover(a.root, b.root, rng)
|
||||
ratios = {**b.ratios, **a.ratios} # primary parent wins
|
||||
else:
|
||||
parent = _tournament(pop, rng)
|
||||
child_root, desc = operators.mutate(parent.root, rng, types,
|
||||
weights=_MUTATION_WEIGHTS)
|
||||
ratios = parent.ratios
|
||||
x0 = innerloop.warm_x0(child_root, ratios)
|
||||
tasks.append((child_root, x0, child_budget, inner_kw, desc))
|
||||
_run_batch(tasks)
|
||||
finally:
|
||||
if _pool is not None:
|
||||
_pool.shutdown(wait=True)
|
||||
|
||||
result.population = sorted(pop, key=lambda i: -i.fitness)
|
||||
result.n_evals = n_evals
|
||||
|
|
|
|||
|
|
@ -146,3 +146,15 @@ def test_random_topology_leaf_count():
|
|||
n_leaves = sum(len(lvl.leaves()) for lvl in dom.levels(topo))
|
||||
assert n_leaves >= n
|
||||
assert n_leaves <= n + 1 # mutate_divide adds exactly one leaf per call
|
||||
|
||||
|
||||
def test_search_parallel_smoke():
|
||||
"""n_workers>1 runs without error and produces valid results."""
|
||||
init_root = dom.load(str(INIT_FILE))
|
||||
r = driver.search(init_root, CORPUS, budget=160, pop_size=2,
|
||||
child_budget=80, seed=0, n_workers=2)
|
||||
assert r.best is not None
|
||||
assert r.best.fitness > 0
|
||||
assert r.n_evals >= 160
|
||||
assert 1 <= len(r.population) <= 2
|
||||
assert r.n_topologies >= 2 # at least the bootstrap individuals
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue