Repository navigation
fix(advantage): AdaptiveTrustRegion op cap no longer ratchets at 1–2; a tighten never loosens; bad knobs refused (#282) - #283
Conversation
…it, a tighten never loosens, bad knobs refused (#282)
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Birfy
left a comment
There was a problem hiding this comment.
Review: LGTM ✅
Thanks, this is a clean, well-scoped fix for #282. I checked out bdeedd0 and probed it locally.
Correctness
_step()fixes the ratchet: a widen is nowmax(cur + 1, round(cur * factor)), so 1 and 2 ops grow again, and the full 6 → 3 → 2 → 3 → 4 → 5 → 6 recovery holds. A tighten ismin(cur - 1, round(cur * factor)), soround(3 * 0.9) == 3no longer counts as a move.- The
min(cur, hi, max(lo, target))form in the tighten branch is right. A cap belowminimumstays where it is, soTrustRegion(3, 500)is no longer quadrupled to 2000 chars by a rollback.maximumstill wins whenminimum > maximum, which keepstest_it_stops_at_its_boundsand the "smallmaximumcaps a run" usage working. Not enforcingminimum <= maximumwas a reasonable call, and the inline comment explains it. - Neutral factors (
1.0) returncur, and nothing is added tohistory. Good. - The constructor now rejects
window=0(which used to raiseZeroDivisionError), inverted or non-finite factors, an out-of-rangeaccept_rate_to_widen, and non-int or non-positive bounds, all withValueErrorand with bools excluded.initialstays unconstrained, as the issue asked.
Tests
tests/test_advantage.py: 21 of the new cases fail onmain(21 failed / 36 passed) and all pass on the branch.test_advantage+test_aggregator+test_ab_rungive 91 passed, and the #224/#226 expectations are unchanged.- CI:
buildand tests on 3.10–3.13 pass. 3.9 was still pending when I looked.
Docs: the class docstring and the CHANGELOG Fixed entry are accurate.
Non-blocking nits
- When
initialis abovemaximum, a widen now narrows the region. For example,TrustRegion(100, 600_000)becomesTrustRegion(64, 512_000)on a clean window. That's the conservative choice, and the code comment says so, but it differs from the issue's "a widen never narrows" and no test pins it. A one-line test for whichever behavior you want would lock it in. - The behavior below
minimumis asymmetric. A tighten holds a sub-minimum cap, but a widen jumps it straight tominimum(chars500 → 2000 in one step atwiden=1.25). This is pre-existing behavior, but it might deserve half a sentence in the "Every move is a move" paragraph. widen=True/tighten=Truestill pass validation, since a bool counts as1, whilewindowexplicitly refuses bools. A non-numeric factor raisesTypeErrorfrommath.isfiniterather thanValueError. Both are trivial.- Pre-existing: when a tighten leaves both caps unchanged (both at their floor),
_recentisn't cleared, so the same reversal is re-counted on each followingobserveuntil it leaves the window. That's harmless, so I'm only noting it.
Good to merge once CI is fully green.
Birfy
left a comment
There was a problem hiding this comment.
Auto-review: LGTM (COMMENT, since self-approve is blocked)
Closes #282. Checked on bdeedd0 against main @ 3947aee.
Correctness
- Op-cap ratchet. On
main,window=1with two reversals and ten clean windows ends atTrustRegion(ops=2, chars=74508), so ops never move._stepwithfactor > 1usesmax(cur + 1, round(cur * factor)), giving 6 → 3 → 2 → 3 → 4 → 5 → 6. That is pinned exactly bytest_two_reversals_then_clean_windows_recover_the_default_cap. - A tighten never loosens.
min(cur, hi, max(lo, target)): a cap belowminimumstays put on a reversal instead of being clamped up. Onmain,initial=(3, 500)plus oneoracle-rejectedgave chars 2000. The ops side still steps down to the floor. maximumstays a hard ceiling on both widen and tighten, and wins overminimumwhere they disagree, which keepstest_it_stops_at_its_bounds(max chars 1000 < default min 2000) passing unchanged. A factor of exactly 1.0 is neutral and appends nothing tohistory.- Validation at construction.
window=0(wasZeroDivisionErroron the firstobserve), float or bool window,widen < 1,tighten∉ (0, 1], NaN/inf factors,accept_rate_to_widen∉ [0, 1], and non-int or < 1 bounds now raise. Not enforcingminimum <= maximumis the right call given the existing pinned usage, and the PR body explains the deviation from the issue.
Tests
- 9 new tests / 26 cases in
tests/test_advantage.py. 21 cases fail onmainand all pass on the branch.window=-1already raised viadeque(maxlen=-1), so it is a guard rather than a regression. test_advantage+test_aggregator+test_ab_runpass, including the #224/#226 adaptive-region regressions withinitialbelowminimum. Full local suite: exit 0.- Exact-head CI: build + 3.9–3.13 green. The Codex review on 97f7042 finished with no findings, and bdeedd0 differs from it only by a blank line.
Docs
- Class docstring states the "every move is a move" rule and the asymmetry. The CHANGELOG Fixed entry is accurate.
docs/api.mdregenerated with no diff (it carries only the summary line).
Nits (non-blocking)
- When ops sit at
maximumand only chars move,historystill records a new value, which is correct but means_fired_counts("trust-region")can count a window that changed only chars. That is pre-existing behaviour, unchanged here. GroupAdvantage.observehas a related numerical edge: one NaN or inf reward silently pins a group's advantage toNonefor the rest of that base version. That is a separate issue, not this PR.
…e-trust-region # Conflicts: # CHANGELOG.md
Birfy
left a comment
There was a problem hiding this comment.
Re-checked after merging main (924c25c, includes #272) at 7c28530. The only conflict was CHANGELOG.md (### Fixed): both entries are kept, the #282 AdaptiveTrustRegion entry above the #271 runstore entry, and the spacing is intact. No code conflicts. tests/test_advantage.py passes locally on the merge commit, and CI is fully green (build + 3.9–3.13). No unresolved review threads. LGTM, still merge-ready.
Stop editing CHANGELOG.md so open PRs no longer conflict on Unreleased.
Closes #282
Problem
AdaptiveTrustRegion.observe()computed each next cap asround(cap * factor)clamped to[minimum, maximum]:widen=1.25,round(2 * 1.25) == round(2.5) == 2(banker's rounding) andround(1 * 1.25) == 1. Two reversals from the default 6 (6 → 3 → 2) left the op cap at 2 for the rest of the run. Every later clean window still grewcharsand appended tohistory, so the region looked adaptive while its binding cap was frozen. Repro on main:window=1, twooracle-rejected, tencommitted→TrustRegion(ops=2, chars=74508).minimum(e.g.initial=TrustRegion(3, 500)as in the OVERSIZED merge report quotes the config constants, not the adaptive trust region that actually rejected the diff #224/AdaptiveTrustRegion ignores OVERSIZED outcomes, so a binding size cap never widens #226 regressions), a reversal computedmax(2000, 250) == 2000and quadrupled the char cap on a rollback.window=0raisedZeroDivisionErroron the firstobserve();widen < 1,tighten > 1, NaN/inf factors, etc. were accepted and silently inverted the rule.Fix
AdaptiveTrustRegion._step(). A widen moves a cap up by at least one unit (max(cur + 1, round(cur * factor))), then clamps to[minimum, maximum]. A tighten moves it down by at least one unit, clamps, and never ends abovecur.maximumremains a hard ceiling on every move (and wins whereminimum > maximum, whichtest_it_stops_at_its_boundsrelies on). Factors of exactly1.0are neutral.__init__validation (ValueError):windowint ≥ 1;widenfinite ≥ 1;tightenfinite in(0, 1];accept_rate_to_widenfinite in[0, 1]; every bound an int ≥ 1. I did not enforceminimum <= maximum(the issue proposed it), because passing only a smallmaximumbelow the defaultminimum.charsis pinned by an existing test as a supported way to cap a run.initialstays unconstrained.docs/api.mdregenerated: no change (it carries only the summary line).Tests
tests/test_advantage.py: 9 new tests / 26 cases, covering widening out of 1- and 2-op caps, a full 6→3→2→3→4→5→6 recovery, one-unit tightening (round(3*0.9)==3), no loosening belowminimum,maximumstill capping a slow widen and a tighten, invalid knobs, and neutral factors. 21 of the new cases fail onmainand all pass on the branch. Every existing trust-region expectation, including #224/#226 intests/test_aggregator.pyandtests/test_ab_run.py, is unchanged:test_advantage+test_aggregator+test_ab_runall pass; full suite result below.