fix(dspy): correct Refine/BestOfN fail_count budget accounting - #78
Open
detail-app[bot] wants to merge 1 commit into
Open
Conversation
Greptile SummaryThis PR corrects failure-budget accounting in
Confidence Score: 5/5The PR appears safe to merge because the corrected accounting matches the documented failure allowance and is covered across both affected modules. No actionable regressions remain; each invocation receives an independent budget, actual failures consume it, and the next failure beyond the configured allowance is re-raised. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Begin forward call] --> B[Copy configured fail_count]
B --> C[Run next rollout]
C --> D{Rollout pipeline succeeds?}
D -- Yes --> E[Evaluate and retain best prediction]
D -- No --> F[Decrement local failure budget]
F --> G{Budget below zero?}
G -- Yes --> H[Raise exception]
G -- No --> I{More rollouts?}
E --> I
I -- Yes --> C
I -- No --> J[Return best prediction]
Reviews (1): Last reviewed commit: "fix(dspy): correct Refine/BestOfN fail_c..." | Re-trigger Greptile |
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.
Warning
GitHub issue creation failed
Detail attempted to publish this bug to GitHub, but the issue could not be created. This fix PR was created without that issue, and missing tracker references are shown as
Unknown issue.You can review and merge this PR normally. Please review your tracker integration settings before the next publish run.
Detail bug report: View on Detail
📝 Changes Description
dspy.Refineanddspy.BestOfNaccept afail_countparameter documented as "The number of times the module can fail before raising an error". The implementation inforward()did not honor that contract:exceptblock compared the rollout indexidx(which increments on every rollout, success or failure) againstself.fail_count, instead of the number of failures that actually occurred. A single transient failure on a late rollout (after several successes) was re-raised even when the actual failure count was still within budget — e.g. withfail_count=2, one failure atidx=4tripped4 > 2and raised.self.fail_count -= 1mutated the instance attribute, so the failure budget leaked acrossforward()calls. After one call tolerated failures, the next call on the same instance started with a depleted (or negative) budget and silently tolerated fewer failures.The bug was introduced in stanfordnlp#7926 (commit
ef32f66d) and went undetected because the accompanying tests only covered failures starting atidx == 0, whereidxand the failure count advance in lockstep.Fix (
dspy/predict/refine.py,dspy/predict/best_of_n.py): take a localfail_count = self.fail_countat the top offorward()(reset per call) and replace theexceptcheck withfail_count -= 1; if fail_count < 0: raise e. This compares actual failures to the budget and never mutatesself.fail_count, fixing both the index-vs-count confusion and the cross-call leak in one change. The defaultfail_count or Nnow tolerates allNrollouts failing (returns the best prediction seen so far,None) rather than raising, matching the pre-stanfordnlp#7926 swallow-on-default behavior and the docstring.Closes Unknown issue.
✅ Contributor Checklist
pre-commit run --files <changed files>exits 0 (ruff lint + check hooks)fix(dspy): correct Refine/BestOfN fail_count budget accounting{label}(dspy): {message}—fix(dspy): correct Refine/BestOfN fail_count budget accountingtest_refine_module_default_fail_count/test_refine_module_default_fail_count(best_of_n) tests encoded the buggy off-by-one behavior (asserting thatfail_count=Nwith an always-failing module raises). They are updated to assert the corrected semantics: with the default, allNfailures are tolerated and the module returnsNone. If the intended contract is instead "raise once failures exceedfail_count" even at the default, the default should be set tofail_count=N+1rather than reverting the accounting logic — flagging here so that decision is explicit.Testing
tests/predict/test_refine.pyandtests/predict/test_best_of_n.py): a single late failure is tolerated withfail_count=2; a second late failure withfail_count=1still re-raises; andself.fail_countis unchanged after one or twoforward()calls (no cross-call leak). The updated default-fail_counttests assert the correctedNone-return semantics and thatself.fail_countis not mutated.git stashof the source fix) and pass on the fixed source, so they genuinely encode the bug rather than passing trivially.uv run ruff checkanduv run ruff format --checkare clean on the changed files;uv run pytest tests/predict/test_refine.py tests/predict/test_best_of_n.py -v→ 12 passed; the broaderuv run pytest tests/predict -m 'not extra and not deno'→ 252 passed, 2 skipped, 0 failed; the full non-live suite (uv run pytest -m 'not extra and not deno' -n auto --dist worksteal tests/) → 1256 passed, 0 failed. No type checker is configured for this repo.ollama servewithqwen3:0.6bpulled) andLM_FOR_TEST=ollama/qwen3:0.6b uv run pytest -m llm_call --llm_call tests/was run → 5 passed, 19 skipped (provider-specifictest_lm_direct_live.pytests that require openai/anthropic/gemini backends), 0 failed. Note: no in-treellm_calltest coversRefine/BestOfNdirectly, so this is a defense-in-depth check of the LM-calling stack rather than direct coverage of this fix; the fix is pure control-flow logic and is backend-agnostic.ruff format --checkreports 109 pre-existing files needing reformatting on the un-modified baseline (identical count with the fix stashed), so this change introduces no new formatting debt;ruff formatis not a CI gate here.Automatic Fixes PRs can be configured here.