Skip to content

fix(mbpp reward): enforce the execution timeout with a forked child - #11

Open
khazic wants to merge 1 commit into
sunrainyg:mainfrom
khazic:khazic/fix/mbpp-reward-hard-timeout
Open

khazic wants to merge 1 commit into
sunrainyg:mainfrom
khazic:khazic/fix/mbpp-reward-hard-timeout

Conversation

@khazic

@khazic khazic commented Sep 3, 2026

Copy link
Copy Markdown

Problem

baselines/verl/utils/reward_score/mbpp.py bounds exec() with a signal.alarm context manager. That guard can only fire between Python bytecodes, so two classes of generated code escape it:

  • code that sits inside one long C-level call (sum(range(10**13)), a huge integer power, a catastrophic regex): the alarm is delivered but the handler never gets to run until the call returns;
  • any evaluation done off the main thread: timeout() detects the non-main thread and silently yields with no limit at all.

In a real run a single candidate whose program never terminated pinned one core at 100% for 7.5 hours and stalled the entire evaluation, with the 5 s alarm armed the whole time.

Fix

execute_code_with_tests now runs its body in a forked child and the parent SIGKILLs the child when the budget is exhausted. The budget is timeout_sec * (1 + len(test_list)), the same upper bound the per-exec alarms already implied. The per-exec alarms are kept inside the child so pure-Python loops still fail fast on their own test. Where os.fork does not exist the previous in-process path is used unchanged, so non-POSIX behaviour is identical.

The result contract (passed, passed_count, total_tests, error) and compute_score are untouched; a killed child reports "Code execution timed out" exactly like an alarm did.

Verification

Local run against the patched module with test_list = ["assert f(2) == 4", "assert f(3) == 9"]:

candidate result time
correct x * x score 1.0, 2/2 0.0 s
wrong x score 0.0, "Some tests failed" 0.0 s
while True: pass 0/2 (per-test alarm, unchanged) 10.0 s
sum(range(10**13)) "Code execution timed out" 15.0 s
loop swallowing BaseException "Code execution timed out" 15.0 s

The unpatched module on the sum(range(10**13)) case does not return within 30 s with its 5 s alarm armed (killed by an external cap).

The mbpp reward guards exec() with signal.alarm, which only fires between
Python bytecodes. Generated code that sits inside one long C-level call
(a huge integer power, sum(range(10**13)), a catastrophic regex) is never
interrupted, and in non-main threads the guard is skipped altogether. In
practice a single non-terminating candidate pinned one core for hours and
stalled a whole evaluation run.

Run the evaluation body in a forked child and SIGKILL it when the budget
(timeout_sec per exec, as before) is exhausted. The per-exec alarms are kept
inside the child so pure-Python loops still fail fast; the fork path is used
only where os.fork exists, other platforms keep the previous behaviour.

Verified locally: normal pass/fail unchanged; a pure-Python infinite loop
still times out per test; sum(range(10**13)) and a loop that swallows
BaseException, both of which hang the old implementation indefinitely, now
return "Code execution timed out" at the budget.

Signed-off-by: khazic <khazzz1c@gmail.com>
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