feat(effort): scale the MPT-enumeration ceiling, not poolMaxSize - #285
Open
ms609 wants to merge 1 commit into
Open
feat(effort): scale the MPT-enumeration ceiling, not poolMaxSize#285ms609 wants to merge 1 commit into
ms609 wants to merge 1 commit into
Conversation
`effort` doubled the replicate budget and the hit target from rung 5, but not the size of the tree set a search returns -- so a run could be given 8x the budget and still return the default 100 trees, the ceiling bounding the answer rather than the MPT count doing so (observed on four 182-tip analyses, all of which returned exactly `poolMaxSize` trees). Scaling `poolMaxSize` itself would have been wrong. During the replicate loop that cap is not a ceiling on what is returned but the size of the working set the search reads: `tree_fuse` takes its donors from the whole pool (uncapped, `max_rounds = 10`, and held under the pool mutex on the parallel path, where a slow fuse stalls every worker), conflict-guided sectorial search calls compute_split_frequencies() over best-score entries once per replicate, and consensusConstrain calls extract_consensus_splits(). Raising it changes which trees the search VISITS, so the anytime-dominance argument that licenses raising `maxReplicates` -- a higher cap only appends later replicates and never delays an earlier improvement -- does not transfer. After the loop the pool is pure output, and there the argument does hold. So split the knob: new `SearchControl(enumMaxTrees =)` is a ceiling raised only at the MPT-enumeration phase, and `.RungSpec()` gains `enumMultiplier`, doubling from rung 5 in step with the other two budgets. `enumMaxTrees = 0` (the default) keeps `poolMaxSize` throughout, so the shipped default is unchanged; an explicit `enumMaxTrees` outranks the ladder, as an explicit `targetHits` already does. The ceiling is only ever raised, never lowered, so entries already held cannot be orphaned above the cap. It needs no policy cap of its own: enumeration shares the `maxSeconds * enumTimeFraction` reserve and its loop exits as soon as the pool fills, so an over-generous value costs enumeration time, never a worse tree. Applied at all three enumeration sites (both in ts_driven.cpp -- the second inherits the raise, which is unconditional -- and the parallel one, which already operates on the extracted OUTPUT pool, leaving the shared search pool's cap alone). `ts_rcpp.cpp` reads the field defensively so a control list built by an older caller is treated as 0 rather than throwing on R_NilValue. Tests: 919 assertions pass across MaximizeParsimony-features, ts-pool, ts-strategy, ts-driven, ts-parallel, ts_collapse_pool and SearchControl, including an end-to-end case that fails if the field is dropped anywhere along SearchControl -> ctrl list -> DrivenParams -> TreePool::raise_max_size(), and a check that raising the ceiling leaves the score unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
effortdoubled the replicate budget and the hit target from rung 5, but not thesize of the tree set a search returns — so a run could be given 8× the budget and
still return the default 100 trees, the ceiling bounding the answer rather than
the MPT count doing so. Observed on four 182-tip analyses, every one of which
returned exactly
poolMaxSizetrees.Why not just scale
poolMaxSize?Because during the replicate loop that cap is not a ceiling on what is returned
— it is the size of the working set the search reads:
tree_fusetakes its donors from the whole pool: uncapped,max_rounds = 10,and on the parallel path held under the pool mutex, where one slow fuse stalls
every worker.
thorough/largesetfuseInterval = 2.compute_split_frequencies()overbest-score entries once per replicate;
consensusConstraincallsextract_consensus_splits();tp.max_hits = max(10, (max_size - size) * 2).So raising it changes which trees the search visits. The anytime-dominance
argument that licenses raising
maxReplicates— "a higher cap only appends laterreplicates and never delays an earlier improvement" — does not transfer to it.
Once the loop is over the pool is pure output, and there the same argument does
hold.
The change
Split the knob rather than scaling the wrong half:
SearchControl(enumMaxTrees = ): a retention ceiling applied to thepost-search MPT-enumeration phase alone.
0(the default) keepspoolMaxSizethroughout, so the shipped default is unchanged.
.RungSpec()gainsenumMultiplier, doubling from rung 5 in step with theother two budgets. An explicit
enumMaxTreesoutranks the ladder, exactly asan explicit
targetHitsalready does, and no preset sets it.TreePool::raise_max_size()only ever raises, so entries already held cannotbe orphaned above the cap. Applied at all three enumeration sites: both in
ts_driven.cpp(the second inherits the raise, which is unconditional) and theparallel one, which already operates on the extracted output pool, so the
shared search pool's cap is untouched by construction.
ts_rcpp.cppreads the field defensively, so a control list built by an oldercaller is treated as
0rather than throwing onR_NilValue.It needs no policy cap of its own: enumeration shares the
maxSeconds * enumTimeFractionreserve and its loop exits as soon as the poolfills, so an over-generous value costs enumeration time, never a worse tree.
As with
hitMultiplier, the doubling shape is an operating point, not a fittedconstant. What is measured is only that the ceiling — not the MPT count — bound
the returned set in all four 182-tip analyses.
Tests
919 assertions pass, 0 failures, across
test-MaximizeParsimony-features.R,test-ts-pool.R,test-ts-strategy.R,test-ts-driven.R,test-ts-parallel.R,test-ts_collapse_pool.Randtest-SearchControl.R.New cases: the ladder scales
enumMultiplierat the same rate as the otherknobs and leaves rungs 1–4 alone; no preset carries a value and an explicit one
survives the merge as
explicit;0is legal but a negative is rejected; and anend-to-end case that fails if the field is dropped anywhere along
SearchControl→ ctrl list →DrivenParams→TreePool::raise_max_size(),which also asserts that raising the ceiling leaves the score unchanged.
Roxygen regenerated only
man/SearchControl.Rdandman/MaximizeParsimony.Rd.Note on authorship: this was written to be committed by
ms609-agent(the commitis attributed to it) so that it would be reviewable, but that account is
currently blocked, so the PR itself is opened by @ms609 at their request. It
therefore cannot be self-approved.
🤖 Generated with Claude Code