-
Notifications
You must be signed in to change notification settings - Fork 84
[CI] add reward-fn cpu tests + cpu-on-main trigger (port slime #1939+#1940) #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| """CPU unit tests for ``vime.rollout.rm_hub.deepscaler``. | ||
|
|
||
| Pins the wrapper that decides which segment of the response counts as | ||
| the "solution" and reduces grading to ``math_utils``. The branching is | ||
| small (3 cases) but silent-failure prone: if the ``</think>`` / | ||
| ``###Response`` markers stop matching the format the rollout actually | ||
| produces, the function returns 0 *for every sample* and no other CI | ||
| signal would catch it. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from vime.rollout.rm_hub.deepscaler import get_deepscaler_rule_based_reward | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_response_split_on_think_marker_grades_tail(): | ||
| """The default chat format puts the answer after ``</think>``. Only | ||
| the tail is graded — pre-think reasoning is ignored, even if it | ||
| contains the wrong answer first.""" | ||
| response = r"Let me reconsider. \boxed{99}</think>Final: \boxed{42}" | ||
| assert get_deepscaler_rule_based_reward(response, "42") == 1 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_response_split_on_response_marker_grades_tail(): | ||
| """Alternate format: ``###Response`` separator. Only what comes after | ||
| is graded (deepscaler.py:7-8).""" | ||
| response = r"Scratch work \boxed{wrong}###Response\boxed{42}" | ||
| assert get_deepscaler_rule_based_reward(response, "42") == 1 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_response_without_any_marker_returns_zero(): | ||
| """No ``</think>`` AND no ``###Response`` → fall through to 0 | ||
| immediately (deepscaler.py:9-10). This is the silent-failure pole — | ||
| if upstream chat templates drop both markers, all rewards become 0.""" | ||
| assert get_deepscaler_rule_based_reward(r"\boxed{42}", "42") == 0 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_response_with_no_boxed_answer_returns_zero(): | ||
| """Marker is present but no ``\\boxed`` in the tail → ``extract_answer`` | ||
| returns None → 0 (deepscaler.py:13-14).""" | ||
| assert get_deepscaler_rule_based_reward("plain</think>no box here", "42") == 0 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_empty_label_returns_zero(): | ||
| """Empty ground-truth → 0 (deepscaler.py:15-16). Guards against | ||
| missing-label data poisoning training with spurious 0s — explicitly | ||
| the same as wrong-answer, intentional.""" | ||
| assert get_deepscaler_rule_based_reward(r"</think>\boxed{42}", "") == 0 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_label_as_int_is_coerced_to_string(): | ||
| """Integer labels are accepted and ``str()``'d (deepscaler.py:19, 25). | ||
| Common case for datasets that store numeric labels.""" | ||
| assert get_deepscaler_rule_based_reward(r"</think>\boxed{42}", 42) == 1 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_label_as_float_is_coerced_to_string(): | ||
| """float labels: stringified to e.g. "42.0". The current grader path | ||
| (mathd or sympy) handles "42.0" vs "42" via normalization — pinning | ||
| the wiring, not the equality logic.""" | ||
| assert get_deepscaler_rule_based_reward(r"</think>\boxed{42}", 42) == 1 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_label_with_boxed_marker_is_extracted_too(): | ||
| """If the ground truth itself is wrapped in ``\\boxed{}``, it must be | ||
| unwrapped before grading (deepscaler.py:26-29).""" | ||
| assert get_deepscaler_rule_based_reward(r"</think>\boxed{42}", r"\boxed{42}") == 1 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_wrong_answer_returns_zero(): | ||
| """Sanity-check the negative side of the contract.""" | ||
| assert get_deepscaler_rule_based_reward(r"</think>\boxed{43}", "42") == 0 | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_grader_uses_either_mathd_or_sympy_path(): | ||
| """``\\frac{1}{2}`` vs ``0.5`` — mathd_normalize collapses both, even | ||
| though the strings aren't lexically equal. Pins the "either mathd OR | ||
| sympy succeeds" disjunction at deepscaler.py:38.""" | ||
| response = r"</think>\boxed{\frac{1}{2}}" | ||
| assert get_deepscaler_rule_based_reward(response, "0.5") == 1 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(pytest.main([__file__])) | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| """CPU unit tests for ``vime.rollout.rm_hub.f1``. | ||
|
|
||
| Pins the token-F1 contract used by the ``f1`` rm_type. The whole pipeline | ||
| is pure Python (regex + ``collections.Counter``), so any silent drift | ||
| here directly distorts a training run's reward signal without touching a | ||
| crash log. Cover the four shapes a reward consumer cares about: | ||
|
|
||
| - normalize_answer: article-strip + punctuation-strip + lowercase + | ||
| whitespace-collapse (the order matters — ``a.`` should normalize to | ||
| `""`, not `"a"`) | ||
| - yes/no/noanswer special-case (exact-match required, not token F1) | ||
| - zero-overlap path (returns the ZERO_METRIC sentinel) | ||
| - non-trivial F1 with hand-derived precision/recall | ||
|
|
||
| The module ships zero existing tests; if any of the regexes or the | ||
| Counter intersection break, no other CI signal would notice. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from vime.rollout.rm_hub.f1 import f1_score, normalize_answer | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| @pytest.mark.parametrize( | ||
| "raw,expected", | ||
| [ | ||
| ("The Quick Brown Fox.", "quick brown fox"), # articles stripped, punc removed, lowercased | ||
| ("An apple, a day", "apple day"), # both "an" and "a" stripped | ||
| ("HELLO WORLD", "hello world"), # whitespace collapsed | ||
| ("a.b,c!", "abc"), # punc adjacent to chars collapses without leaving a space | ||
| ("", ""), # empty input survives | ||
| ("the", ""), # all-article input collapses to empty | ||
| ], | ||
| ) | ||
| def test_normalize_answer(raw, expected): | ||
| assert normalize_answer(raw) == expected | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_f1_exact_match_is_perfect(): | ||
| """Hand-derived: tokens fully overlap → precision=recall=f1=1.0.""" | ||
| f1, p, r = f1_score("Paris is the capital", "Paris is the capital") | ||
| assert (f1, p, r) == (1.0, 1.0, 1.0) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_f1_partial_overlap_hand_derived(): | ||
| """Hand-derived: prediction "the brown fox" → ["brown", "fox"] after | ||
| normalize; ground truth "a quick brown fox" → ["quick", "brown", "fox"]. | ||
| Intersection = {"brown", "fox"} → num_same = 2. | ||
| precision = 2 / 2 = 1.0 (len(pred_tokens) = 2) | ||
| recall = 2 / 3 | ||
| f1 = 2 * 1.0 * (2/3) / (1.0 + 2/3) = 0.8 | ||
| """ | ||
| f1, p, r = f1_score("the brown fox", "a quick brown fox") | ||
| assert p == pytest.approx(1.0) | ||
| assert r == pytest.approx(2 / 3) | ||
| assert f1 == pytest.approx(0.8) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_f1_no_token_overlap_returns_zero_metric(): | ||
| """Disjoint vocabularies → ZERO_METRIC sentinel (0, 0, 0).""" | ||
| assert f1_score("apple banana", "carrot date") == (0, 0, 0) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_f1_none_prediction_returns_zero_metric(): | ||
| """A failed/missing prediction is a common rm path — must be zero, not raise.""" | ||
| assert f1_score(None, "anything") == (0, 0, 0) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| @pytest.mark.parametrize("special", ["yes", "no", "noanswer"]) | ||
| def test_f1_special_token_pred_mismatch_returns_zero(special): | ||
| """yes/no/noanswer in the prediction but not the ground truth — must be | ||
| zero even if token-F1 would otherwise be non-zero. Pins the asymmetric | ||
| early-exit at f1.py:33.""" | ||
| assert f1_score(special, "some other phrase") == (0, 0, 0) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| @pytest.mark.parametrize("special", ["yes", "no", "noanswer"]) | ||
| def test_f1_special_token_gt_mismatch_returns_zero(special): | ||
| """Mirror check: special tokens in the ground truth (f1.py:35).""" | ||
| assert f1_score("some other phrase", special) == (0, 0, 0) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_f1_special_token_exact_match_uses_token_path(): | ||
| """When prediction == ground_truth == "yes", the special-case early-exit | ||
| does NOT fire (it has ``!=`` guards), so we land on the token-F1 path | ||
| with a single common token → f1 = 1.0.""" | ||
| f1, p, r = f1_score("yes", "yes") | ||
| assert (f1, p, r) == (1.0, 1.0, 1.0) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(pytest.main([__file__])) |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test is intended to verify float labels, but it passes the integer
42instead of the float42.0. This makes it identical totest_label_as_int_is_coerced_to_stringand fails to actually test float coercion.