@@ -344,9 +344,24 @@ def run_code(code: str, test_code: str = "", timeout_s: float = 5.0) -> Executio
344344 if err := _validate_ast (full_code ):
345345 return ExecutionResult (exception = err , passed = False )
346346
347- # Run in child process for isolation
348- q : multiprocessing .Queue = multiprocessing .Queue ()
349- proc = multiprocessing .Process (
347+ # Run in child process for isolation.
348+ #
349+ # Explicitly use the "spawn" start method rather than the platform
350+ # default. On Linux, multiprocessing.Process() defaults to fork(),
351+ # which copies the ENTIRE parent memory image into every child --
352+ # including PyTorch, if anything in the same test/training session
353+ # already imported it (e.g. RewardMLP tests running earlier in the
354+ # same pytest process). Combined with coverage.py tracing every
355+ # forked child, running ~200 sandboxed executions in one CI session
356+ # compounded into an OOM kill on GitHub's 7GB runner, confirmed by
357+ # the kill point moving later (not disappearing) after the first
358+ # resource-cleanup fix -- the leak was real but this was the larger
359+ # cause. spawn starts each child as a fresh, minimal interpreter that
360+ # does not inherit torch or the parent's coverage trace state, at the
361+ # cost of slightly higher per-call startup time versus fork.
362+ ctx = multiprocessing .get_context ("spawn" )
363+ q : multiprocessing .Queue = ctx .Queue ()
364+ proc = ctx .Process (
350365 target = _run_in_process ,
351366 args = (full_code , full_test , q ),
352367 daemon = True ,
0 commit comments