Skip to content

gsm8k: return None when flexible extraction finds no real number - #1

Open
eeshsaxena wants to merge 1 commit into
AfterQuery:mainfrom
eeshsaxena:fix/gsm8k-flexible-invalid-answer
Open

gsm8k: return None when flexible extraction finds no real number#1
eeshsaxena wants to merge 1 commit into
AfterQuery:mainfrom
eeshsaxena:fix/gsm8k-flexible-invalid-answer

Conversation

@eeshsaxena

@eeshsaxena eeshsaxena commented Aug 11, 2026

Copy link
Copy Markdown

In extract_solution(method="flexible") the loop reuses final_answer as the loop variable:

final_answer = None
...
for final_answer in reversed(answer):
    if final_answer not in invalid_str:
        break

When every regex match is an invalid token ("" or "."), the loop never breaks, so final_answer ends up bound to the last iterated token instead of the None it was initialised to.

extract_solution("The result is just a period .", "flexible")  # -> '.'  (should be None)

compute_score then treats that as a real-but-wrong answer and returns format_score instead of 0, so a non-zero format_score leaks to responses that contain no number at all.

Fix iterates over a separate variable and only assigns final_answer for a valid candidate, leaving it None when nothing valid is found. Valid inputs are unaffected (it still returns the last real number).

Added tests/utils/reward_score/test_gsm8k_on_cpu.py covering the valid cases plus the no-real-number cases. The stray-period case returns '.' on main and None here.

Note: I couldn't run the full verl suite locally (no ray/torch/GPU on my machine), so I verified by exercising verl/utils/reward_score/gsm8k.py directly: with the fix the new cases pass and compute_score("just a period .", "42", method="flexible", format_score=0.1) returns 0 instead of 0.1. The added test is plain CPU and should run in CI.

Summary by CodeRabbit

  • Bug Fixes

    • Improved numeric answer extraction for GSM8K scoring.
    • Correctly handles valid numbers, negative values, and responses containing multiple candidates.
    • Responses without a valid numeric answer now receive a zero format score.
  • Tests

    • Added CPU coverage for valid, invalid-only, negative, and digitless answers.

In extract_solution(method="flexible") the loop reused final_answer as its
loop variable:

    for final_answer in reversed(answer):
        if final_answer not in invalid_str:
            break

When every regex match is an invalid token ("" or ".") the loop never breaks,
so final_answer is left bound to the last iterated (invalid) token instead of
the None it was initialized to. extract_solution("... .", "flexible") returned
".", and compute_score then treated the output as a real-but-wrong answer,
leaking format_score to responses that contain no number.

Iterate over a separate variable and only assign final_answer for a valid
candidate, so it stays None otherwise.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

GSM8K flexible extraction now stores the selected valid numeric answer before stopping reverse iteration. New CPU tests cover positive, negative, invalid-only, and digitless inputs, plus zero scoring without a valid answer.

Changes

GSM8K answer extraction

Layer / File(s) Summary
Answer extraction and validation
verl/utils/reward_score/gsm8k.py, tests/utils/reward_score/test_gsm8k_on_cpu.py
The extractor stores the first valid candidate found in reverse order. Tests cover valid numbers, negative values, invalid-only input, digitless input, and zero format scoring.
Estimated code review effort: 2 (Simple) ~10 minutes
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: returning None when flexible GSM8K extraction finds no valid number.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/utils/reward_score/test_gsm8k_on_cpu.py (1)

20-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover a valid number before a trailing invalid token.

Add ("The answer is 42 .", "42"). The current period case has no valid candidate, so it does not verify that reverse iteration skips "." and returns the previous valid number.

Proposed test
         ("The result is just a period .", None),
+        ("The answer is 42 .", "42"),
         ("no digits at all", None),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/reward_score/test_gsm8k_on_cpu.py` around lines 20 - 33, Add the
missing parametrized case to test_extract_solution_flexible: use input "The
answer is 42 ." with expected result "42", preserving the existing
invalid-period case and other coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@verl/utils/reward_score/gsm8k.py`:
- Around line 46-48: Update the candidate-selection loop around final_answer to
accept a candidate only when it represents a valid number, rather than checking
only invalid_str membership; preserve the reversed(answer) search and stop
assigning once the first valid numeric candidate is found, leaving final_answer
unset or at its existing default when none qualifies.

---

Nitpick comments:
In `@tests/utils/reward_score/test_gsm8k_on_cpu.py`:
- Around line 20-33: Add the missing parametrized case to
test_extract_solution_flexible: use input "The answer is 42 ." with expected
result "42", preserving the existing invalid-period case and other coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b381facd-2a20-49b3-b80e-319b1d929971

📥 Commits

Reviewing files that changed from the base of the PR and between 8497391 and e79cb12.

📒 Files selected for processing (2)
  • tests/utils/reward_score/test_gsm8k_on_cpu.py
  • verl/utils/reward_score/gsm8k.py

Comment on lines +46 to +48
for candidate in reversed(answer):
if candidate not in invalid_str:
final_answer = candidate

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate candidates as numbers before assigning final_answer.

The regex at Line 38 also matches punctuation-only strings such as "..", ",", and "-.". The current condition rejects only exact "" and ".", so the loop can still return an invalid token when no real number exists. Validate each candidate before breaking.

Proposed fix
-            invalid_str = ["", "."]
-            # find the last number that is not '.'
+            # find the last parseable number
             for candidate in reversed(answer):
-                if candidate not in invalid_str:
-                    final_answer = candidate
-                    break
+                try:
+                    float(candidate.replace(",", ""))
+                except ValueError:
+                    continue
+                final_answer = candidate
+                break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@verl/utils/reward_score/gsm8k.py` around lines 46 - 48, Update the
candidate-selection loop around final_answer to accept a candidate only when it
represents a valid number, rather than checking only invalid_str membership;
preserve the reversed(answer) search and stop assigning once the first valid
numeric candidate is found, leaving final_answer unset or at its existing
default when none qualifies.

@eeshsaxena

Copy link
Copy Markdown
Author

Hi! Gentle nudge on this one whenever you have some bandwidth. It's a small, self-contained fix (gsm8k: return None when flexible extraction finds no real number), and it's currently mergeable with no conflicts. No urgency at all, and I'm happy to make any changes you'd like. Thanks for maintaining verl!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant