From e79cb12026234957f60651562668d795af308ddd Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Tue, 11 Aug 2026 21:46:49 +0530 Subject: [PATCH] gsm8k: return None when flexible extraction finds no real number 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. --- tests/utils/reward_score/test_gsm8k_on_cpu.py | 40 +++++++++++++++++++ verl/utils/reward_score/gsm8k.py | 5 ++- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/utils/reward_score/test_gsm8k_on_cpu.py diff --git a/tests/utils/reward_score/test_gsm8k_on_cpu.py b/tests/utils/reward_score/test_gsm8k_on_cpu.py new file mode 100644 index 00000000000..a60530d9fe2 --- /dev/null +++ b/tests/utils/reward_score/test_gsm8k_on_cpu.py @@ -0,0 +1,40 @@ +# Copyright 2024 Bytedance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from verl.utils.reward_score import gsm8k + + +@pytest.mark.parametrize( + "solution_str, expected", + [ + ("The answer is 42", "42"), + ("first 7 then 13", "13"), + ("negative -5 here", "-5"), + # Only invalid tokens (a stray period): there is no real number, so the + # extraction must return None rather than the invalid "." token. + ("The result is just a period .", None), + ("no digits at all", None), + ], +) +def test_extract_solution_flexible(solution_str, expected): + assert gsm8k.extract_solution(solution_str, method="flexible") == expected + + +def test_flexible_no_valid_number_scores_zero(): + # A stray "." used to be extracted as the answer, so a non-zero format_score + # leaked to outputs that contain no real number. With no valid number the + # answer is None and the score is 0. + assert gsm8k.compute_score("just a period .", "42", method="flexible", format_score=0.1) == 0 diff --git a/verl/utils/reward_score/gsm8k.py b/verl/utils/reward_score/gsm8k.py index 98a8c24dc8c..f86165560a6 100644 --- a/verl/utils/reward_score/gsm8k.py +++ b/verl/utils/reward_score/gsm8k.py @@ -43,8 +43,9 @@ def extract_solution(solution_str, method="strict"): else: invalid_str = ["", "."] # find the last number that is not '.' - for final_answer in reversed(answer): - if final_answer not in invalid_str: + for candidate in reversed(answer): + if candidate not in invalid_str: + final_answer = candidate break return final_answer