diff --git a/README.md b/README.md index a7cafa94..5b91557f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ https://github.com/user-attachments/assets/50a8de5a-b519-4aef-8e54-c60ac9dcbb90 - [Evaluation](#evaluation) - [Documentation](#documentation) - [Development and Contributing](#development-and-contributing) +- [License](#license) - [Citation](#citation) ## Setup @@ -224,6 +225,13 @@ For more details about ALE-Bench, please refer to the [docs/](./docs/) directory ## Development and Contributing Please see the [CONTRIBUTING.md](./CONTRIBUTING.md) file. +## License + +The code in this GitHub repository is licensed under the Apache License 2.0. +See the [LICENSE](./LICENSE) file for details. + +The ALE-Bench dataset hosted on [Hugging Face](https://huggingface.co/datasets/SakanaAI/ALE-Bench) is licensed under the [Creative Commons Attribution-NoDerivatives 4.0 International License (CC BY-ND 4.0)](https://creativecommons.org/licenses/by-nd/4.0/). + ## Citation Please cite ALE-Bench as follows: diff --git a/docs/evaluation.md b/docs/evaluation.md index 03f41690..844b88b7 100644 --- a/docs/evaluation.md +++ b/docs/evaluation.md @@ -120,6 +120,7 @@ bash scripts/run_eval.sh gpt-5 --max_concurrent_llm_calls 20 --max_repeated_samp | `n_repeated_sampling` | int | 1 | Number of repeated sampling iterations | | `n_self_refine` | int | 1 | Number of self-refinement iterations including repeated sampling process (`1` means no self-refinement) | | `num_workers` | int | 1 | Number of parallel case evaluation workers for each problem | +| `reuse_containers` | bool | `False` | Reuse long-lived execution and tool containers across cases instead of creating per-case containers | | `n_public_cases` | int | `None` | Number of cases to use for public evaluation (`None` means using ALE-Bench default: 50 for `all`, 5 for `lite`) | | `judge_version` | str | `202301` | Judge toolchain version (`201907`, `202301`, `202510`) | | `code_language` | str | `cpp20` | Target programming language (`any`, `bash`, `cpp17`, `cpp20`, `cpp23`, `csharp`, `fish`, `fortran`, `go`, `haskell`, `javascript`, `julia`, `lean`, `ocaml`, `perl`, `pypy`, `python`, `rust`, `typescript`) | @@ -135,6 +136,10 @@ bash scripts/run_eval.sh gpt-5 --max_concurrent_llm_calls 20 --max_repeated_samp > **Note**: Ensure that `num_workers` $\times$ `max_parallel_problems` does not exceed the number of physical CPU cores available on your machine to avoid resource contention and performance degradation. +> **Note**: Near time-limit boundaries, ALE-Bench results can be sensitive to measurement granularity and host load. Execution time is derived from GNU `/usr/bin/time`. Values around the limit, especially within about 0.01 seconds, may flip between AC and TLE. A run that exceeds the time limit by less than 0.01 seconds may still be reported as AC. + +> **Note**: `reuse_containers=True` reduces Docker create/remove overhead by keeping up to `num_workers` execution/tool containers alive and dispatching cases to whichever worker becomes free. It is opt-in because writable container-layer state such as files under `/tmp` can persist between cases assigned to the same worker. + > **Note**: `max_parallel_problems` controls problem-level concurrency. `max_repeated_sampling_workers` controls only repeated-sampling LLM generation within each problem. If it is `None`, it is resolved to `n_repeated_sampling`; otherwise it is capped at `n_repeated_sampling`. `max_concurrent_llm_calls` is a global cap shared by repeated sampling and self-refinement LLM calls. If it is `None`, it is resolved to `max_parallel_problems * effective_max_repeated_sampling_workers`. Judge execution remains bounded by `num_workers` for each active problem. > **Note**: `code_language` must be supported by the selected `judge_version`. diff --git a/docs/session_object.md b/docs/session_object.md index cfad6f05..6b5594ee 100644 --- a/docs/session_object.md +++ b/docs/session_object.md @@ -86,10 +86,13 @@ Evaluates the provided code against the given input string(s). This method is in - `time_limit (float, optional)`: Custom time limit for execution in seconds. Defaults to `None` (uses problem-specific default). - `memory_limit (int | str, optional)`: Custom memory limit for execution (e.g., `256_000_000` for 256MB, or "256m"). Defaults to `None` (uses problem-specific default). - `skip_local_visualization (bool, optional)`: If `True`, skips generating local visualizations even if available. Defaults to `False`. +- `reuse_containers (bool, optional)`: If `True`, reuses long-lived execution and tool containers for this call. Defaults to `False`. **Returns:** - `Result`: A `Result` object containing the evaluation details, including scores, execution time, and memory usage for each case. +`reuse_containers=True` is also available on `case_gen_eval()`, `public_eval()`, and `private_eval()`. It avoids per-case execution/tool container create/remove overhead, but writable container-layer state such as files under `/tmp` may persist between cases assigned to the same worker. + --- ### `case_gen_eval` A convenience method that first generates test case(s) using specified seeds and generation arguments, and then immediately evaluates the provided code against these newly generated cases. @@ -103,6 +106,7 @@ A convenience method that first generates test case(s) using specified seeds and - `memory_limit (int | str, optional)`: Custom memory limit. Defaults to `None`. - `gen_kwargs (dict, optional)`: Arguments for the case generator. Defaults to an empty dictionary. - `skip_local_visualization (bool, optional)`: If `True`, skips local visualizations. Defaults to `False`. +- `reuse_containers (bool, optional)`: If `True`, reuses long-lived execution and tool containers for this call. Defaults to `False`. **Returns:** - `Result`: A `Result` object with the evaluation outcome. @@ -127,6 +131,7 @@ Evaluates the provided code against the predefined set of public test cases for - `code_language (CodeLanguage | str)`: The programming language of the code. - `judge_version (JudgeVersion | str, optional)`: The judge version. Defaults to `None` (`202301`). - `skip_local_visualization (bool, optional)`: If `True`, skips local visualizations. Defaults to `True` for public evaluations. +- `reuse_containers (bool, optional)`: If `True`, reuses long-lived execution and tool containers for this call. Defaults to `False`. **Returns:** - `Result`: A `Result` object detailing the performance on public test cases. @@ -139,6 +144,7 @@ Evaluates the provided code against the predefined set of private test cases. Th - `code (str)`: The source code to evaluate. - `code_language (CodeLanguage | str)`: The programming language of the code. - `judge_version (JudgeVersion | str, optional)`: The judge version. Defaults to `None` (`202301`). +- `reuse_containers (bool, optional)`: If `True`, reuses long-lived execution and tool containers for this call. Defaults to `False`. **Returns:** - `Result`: A `Result` object detailing the performance on private test cases. diff --git a/llm_configs/claude-fable-5-high.json b/llm_configs/claude-fable-5-high.json new file mode 100644 index 00000000..44658cd6 --- /dev/null +++ b/llm_configs/claude-fable-5-high.json @@ -0,0 +1,12 @@ +{ + "model_name": "claude-fable-5", + "provider": "anthropic", + "settings": { + "max_tokens": 128000, + "anthropic_thinking": { + "type": "adaptive", + "display": "summarized" + }, + "anthropic_effort": "high" + } +} diff --git a/llm_configs/glm-5.2-high.json b/llm_configs/glm-5.2-high.json new file mode 100644 index 00000000..35cb9c23 --- /dev/null +++ b/llm_configs/glm-5.2-high.json @@ -0,0 +1,23 @@ +{ + "model_name": "z-ai/glm-5.2", + "provider": "openrouter", + "settings": { + "temperature": 1.0, + "top_p": 0.95, + "extra_body": { + "provider": { + "allow_fallbacks": false, + "data_collection": "deny", + "order": ["z-ai"], + "require_parameters": true + }, + "reasoning": { + "effort": "high", + "enabled": true + }, + "usage": { + "include": true + } + } + } +} diff --git a/llm_configs/glm-5.2-max.json b/llm_configs/glm-5.2-max.json new file mode 100644 index 00000000..bacc5c30 --- /dev/null +++ b/llm_configs/glm-5.2-max.json @@ -0,0 +1,23 @@ +{ + "model_name": "z-ai/glm-5.2", + "provider": "openrouter", + "settings": { + "temperature": 1.0, + "top_p": 0.95, + "extra_body": { + "provider": { + "allow_fallbacks": false, + "data_collection": "deny", + "order": ["z-ai"], + "require_parameters": true + }, + "reasoning": { + "effort": "xhigh", + "enabled": true + }, + "usage": { + "include": true + } + } + } +} diff --git a/llm_configs/kimi-k2.7-code.json b/llm_configs/kimi-k2.7-code.json new file mode 100644 index 00000000..84628c3d --- /dev/null +++ b/llm_configs/kimi-k2.7-code.json @@ -0,0 +1,20 @@ +{ + "model_name": "moonshotai/kimi-k2.7-code", + "provider": "openrouter", + "settings": { + "extra_body": { + "provider": { + "allow_fallbacks": false, + "data_collection": "deny", + "order": ["moonshotai/int4"], + "require_parameters": true + }, + "reasoning": { + "enabled": true + }, + "usage": { + "include": true + } + } + } +} diff --git a/llm_configs/ling-2.6-1t.json b/llm_configs/ling-2.6-1t.json new file mode 100644 index 00000000..4750d9e6 --- /dev/null +++ b/llm_configs/ling-2.6-1t.json @@ -0,0 +1,17 @@ +{ + "model_name": "inclusionai/ling-2.6-1t", + "provider": "openrouter", + "settings": { + "extra_body": { + "provider": { + "allow_fallbacks": false, + "data_collection": "deny", + "order": ["novita"], + "require_parameters": true + }, + "usage": { + "include": true + } + } + } +} diff --git a/llm_configs/ring-2.6-1t-high.json b/llm_configs/ring-2.6-1t-high.json new file mode 100644 index 00000000..fb86c20d --- /dev/null +++ b/llm_configs/ring-2.6-1t-high.json @@ -0,0 +1,21 @@ +{ + "model_name": "inclusionai/ring-2.6-1t", + "provider": "openrouter", + "settings": { + "extra_body": { + "provider": { + "allow_fallbacks": false, + "data_collection": "deny", + "order": ["novita"], + "require_parameters": true + }, + "reasoning": { + "effort": "high", + "enabled": true + }, + "usage": { + "include": true + } + } + } +} diff --git a/src/ale_bench/session.py b/src/ale_bench/session.py index 998c9632..feea828f 100644 --- a/src/ale_bench/session.py +++ b/src/ale_bench/session.py @@ -285,6 +285,7 @@ def case_eval( time_limit: float | None = None, memory_limit: int | str | None = None, skip_local_visualization: bool = False, + reuse_containers: bool = False, ) -> Result: """Evaluate the code with the given input. @@ -298,6 +299,8 @@ def case_eval( time_limit (float, optional): The time limit in seconds. Defaults to None. memory_limit (int | str, optional): The memory limit in bytes. Defaults to None. skip_local_visualization (bool, optional): Whether to skip local visualization. Defaults to False. + reuse_containers (bool, optional): Whether to reuse execution and tool containers across cases. + Defaults to False. Returns: Result: The result of the evaluation. @@ -336,6 +339,7 @@ def case_eval( return_details=True, skip_local_visualization=skip_local_visualization, num_workers=self.num_workers, + reuse_containers=reuse_containers, ) # Postprocessing @@ -384,6 +388,7 @@ def case_gen_eval( memory_limit: int | str | None = None, gen_kwargs: dict[str, Any] | None = None, skip_local_visualization: bool = False, + reuse_containers: bool = False, ) -> Result: """Generate a case and evaluate the code with the given input. @@ -396,6 +401,8 @@ def case_gen_eval( memory_limit (int | str, optional): The memory limit in bytes. Defaults to None. gen_kwargs (dict[str, Any]): The generation arguments. Defaults to an empty dictionary. skip_local_visualization (bool, optional): Whether to skip local visualization. Defaults to False. + reuse_containers (bool, optional): Whether to reuse execution and tool containers across cases. + Defaults to False. Returns: Result: The result of the evaluation. @@ -421,7 +428,14 @@ def case_gen_eval( # Generation and evaluation (postprocessing is done in each function) input_str = self.case_gen(seed, gen_kwargs=gen_kwargs) result = self.case_eval( - input_str, code, code_language, judge_version, time_limit, memory_limit, skip_local_visualization + input_str=input_str, + code=code, + code_language=code_language, + judge_version=judge_version, + time_limit=time_limit, + memory_limit=memory_limit, + skip_local_visualization=skip_local_visualization, + reuse_containers=reuse_containers, ) if not self._check_within_resource_usage_after(AleBenchFunction.CASE_GEN_EVAL): # NOTE: maybe this block is not reached because we check the resource usage in each function @@ -485,6 +499,7 @@ def public_eval( code_language: CodeLanguage | str, judge_version: JudgeVersion | str | None = None, skip_local_visualization: bool = True, + reuse_containers: bool = False, ) -> Result: """Evaluate the public score of the submission. @@ -493,6 +508,8 @@ def public_eval( code_language (CodeLanguage | str): The code language. judge_version (JudgeVersion | str, optional): The judge version. Defaults to None (202301). skip_local_visualization (bool, optional): Whether to skip local visualization. Defaults to True. + reuse_containers (bool, optional): Whether to reuse execution and tool containers across cases. + Defaults to False. Returns: Result: The result of the evaluation. @@ -527,6 +544,7 @@ def public_eval( return_details=True, skip_local_visualization=skip_local_visualization, num_workers=self.num_workers, + reuse_containers=reuse_containers, ) # Postprocessing @@ -564,6 +582,7 @@ def private_eval( code: str, code_language: CodeLanguage | str, judge_version: JudgeVersion | str | None = None, + reuse_containers: bool = False, ) -> tuple[Result, int, int]: """Evaluate the private score of the submission. @@ -571,6 +590,8 @@ def private_eval( code (str): The code to evaluate. code_language (CodeLanguage | str): The code language. judge_version (JudgeVersion | str, optional): The judge version. Defaults to None (202301). + reuse_containers (bool, optional): Whether to reuse execution and tool containers across cases. + Defaults to False. Returns: Result: The result of the evaluation. @@ -607,6 +628,7 @@ def private_eval( return_details=False, skip_local_visualization=True, num_workers=self.num_workers, + reuse_containers=reuse_containers, ) # Postprocessing diff --git a/src/ale_bench/tool_wrappers/case_runner.py b/src/ale_bench/tool_wrappers/case_runner.py index 592ba6c3..cef49adb 100644 --- a/src/ale_bench/tool_wrappers/case_runner.py +++ b/src/ale_bench/tool_wrappers/case_runner.py @@ -4,9 +4,11 @@ import math import os import re +import shlex import tempfile import time from concurrent.futures import ThreadPoolExecutor, as_completed +from contextlib import nullcontext from pathlib import Path from pydantic import BaseModel, ConfigDict, Field @@ -24,8 +26,14 @@ ) from ale_bench.data import ProblemType from ale_bench.result import CaseResult, JudgeResult, Profiles +from ale_bench.tool_wrappers.reusable_container_pool import ( + ReusableSubmissionContainerPool, + ReusableToolContainerPool, +) from ale_bench.utils import docker_client, read_svg +TIMEOUT_EXIT_CODE = 124 + class HostPathsCompile(BaseModel): """Paths on the host for the compilation step of the submission.""" @@ -189,24 +197,34 @@ def get_batch_run_volumes(host_paths: HostPathsBatchRun, temp_dir: Path) -> dict } -def build_batch_run_command(code_language: CodeLanguage, judge_version: JudgeVersion, time_limit: float) -> str: +def build_batch_run_command( + code_language: CodeLanguage, + judge_version: JudgeVersion, + time_limit: float, + input_file: str = ale_bench.constants.INPUT_FILE, + output_file: str = ale_bench.constants.OUTPUT_FILE, + profiles_file: str = ale_bench.constants.PROFILES_FILE, +) -> str: """Build the run command for the given code language and judge version. Args: code_language (CodeLanguage): The code language. judge_version (JudgeVersion): The judge version. time_limit (float): The time limit in seconds. + input_file (str): The input file path in the container. + output_file (str): The output file path in the container. + profiles_file (str): The profiles file path in the container. Returns: str: The run command. """ run_command = get_run_command(code_language, judge_version) - run_command += f" < {ale_bench.constants.INPUT_FILE} > {ale_bench.constants.OUTPUT_FILE}" + run_command += f" < {input_file} > {output_file}" run_command = ( "/usr/bin/time " f'-f "{ale_bench.constants.TIME_OUTPUT_FORMAT}" ' - f"-o {ale_bench.constants.PROFILES_FILE} {run_command}" + f"-o {profiles_file} {run_command}" ) # NOTE: We use the GNU Time to measure the resource usage # NOTE: the profiles by GNU Time update every 1 sec (from observations while debugging) time_limit_ceil = math.ceil(time_limit + 0.1) @@ -266,14 +284,17 @@ def get_batch_judge_volumes(host_paths: HostPathsBatchJudge, tool_dir: Path) -> } -def build_batch_judge_command() -> str: +def build_batch_judge_command( + input_file: str = ale_bench.constants.INPUT_FILE, + output_file: str = ale_bench.constants.OUTPUT_FILE, +) -> str: """Build the judging command. Returns: str: The judging command. """ - return f"{ale_bench.constants.TESTER_BIN} {ale_bench.constants.INPUT_FILE} {ale_bench.constants.OUTPUT_FILE}" + return f"{ale_bench.constants.TESTER_BIN} {input_file} {output_file}" class HostPathsReactiveJudge(BaseModel): @@ -358,24 +379,34 @@ def get_reactive_judge_volumes( } -def build_reactive_judge_command(code_language: CodeLanguage, judge_version: JudgeVersion, time_limit: float) -> str: +def build_reactive_judge_command( + code_language: CodeLanguage, + judge_version: JudgeVersion, + time_limit: float, + input_file: str = ale_bench.constants.INPUT_FILE, + output_file: str = ale_bench.constants.OUTPUT_FILE, + profiles_file: str = ale_bench.constants.PROFILES_FILE, +) -> str: """Build the run command for the given code language and judge version. Args: code_language (CodeLanguage): The code language. judge_version (JudgeVersion): The judge version. time_limit (float): The time limit in seconds. + input_file (str): The input file path in the container. + output_file (str): The output file path in the container. + profiles_file (str): The profiles file path in the container. Returns: str: The run command. """ run_command = get_run_command(code_language, judge_version) - run_command += f" < {ale_bench.constants.INPUT_FILE} > {ale_bench.constants.OUTPUT_FILE}" + run_command += f" < {input_file} > {output_file}" run_command = ( f"{ale_bench.constants.TESTER_BIN} /usr/bin/time " f'-f "{ale_bench.constants.TIME_OUTPUT_FORMAT}" ' - f"-o {ale_bench.constants.PROFILES_FILE} {run_command}" + f"-o {profiles_file} {run_command}" ) # NOTE: We use the GNU Time to measure the resource usage # NOTE: the profiles by GNU Time update every 1 sec (from observations while debugging) time_limit_ceil = math.ceil(time_limit + 0.1) @@ -459,14 +490,17 @@ def get_vis_volumes(host_paths: HostPathsVis, tool_dir: Path) -> dict[str, dict[ } -def build_vis_command() -> str: +def build_vis_command( + input_file: str = ale_bench.constants.INPUT_FILE, + output_file: str = ale_bench.constants.OUTPUT_FILE, +) -> str: """Build the visualization command. Returns: str: The visualization command. """ - return f"{ale_bench.constants.VIS_BIN} {ale_bench.constants.INPUT_FILE} {ale_bench.constants.OUTPUT_FILE}" + return f"{ale_bench.constants.VIS_BIN} {input_file} {output_file}" def run_compile_container( @@ -636,6 +670,39 @@ def run_batch_run_container( return execution_time_host, stderr # Run succeeded, return the execution time and stderr +def run_batch_run_reusable_container( + reusable_submission_container_pool: ReusableSubmissionContainerPool, + time_limit: float, + run_command: str, + input_str: str | None, +) -> CaseResult | tuple[float, str]: + """Run the batch submission command in a reusable Docker container.""" + execution_time_host, exit_code, stderr = reusable_submission_container_pool.run(run_command) + if exit_code != 0: + if execution_time_host > time_limit: # Killed by `timeout` command + return CaseResult( + input_str=input_str, + output_str=None, + error_str=stderr if input_str is not None else None, + judge_result=JudgeResult.TIME_LIMIT_EXCEEDED, + message="Time limit exceeded.", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=min(execution_time_host, time_limit + 0.1), # NOTE: slight longer than time limit + memory_usage=0, + ) + return CaseResult( + input_str=input_str, + output_str=None, + error_str=stderr if input_str is not None else None, + judge_result=JudgeResult.RUNTIME_ERROR, + message="Runtime error.", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=execution_time_host, + memory_usage=0, + ) + return execution_time_host, stderr + + def run_batch_judge_container( judge_volumes: dict[str, dict[str, str]], judge_command: str, @@ -720,6 +787,55 @@ def run_batch_judge_container( return int(score_match.group(1)) +def run_batch_judge_reusable_container( + reusable_tool_container_pool: ReusableToolContainerPool, + judge_command: str, + execution_time_host: float, + input_str: str | None, + output_str: str | None, + error_str: str | None, +) -> CaseResult | int: + """Run the batch judge command in a reusable Docker tool container.""" + _execution_time_host_judge, exit_code, stderr = reusable_tool_container_pool.run(judge_command) + if exit_code != 0: + return CaseResult( + input_str=input_str, + output_str=output_str, + error_str=error_str, + judge_result=JudgeResult.WRONG_ANSWER, + message=f"Wrong answer.\nStandard error:\n{stderr}", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=execution_time_host, + memory_usage=0, + ) + if "wrong answer: " in stderr: + error_message = stderr.split("wrong answer: ")[1] + return CaseResult( + input_str=input_str, + output_str=output_str, + error_str=error_str, + judge_result=JudgeResult.WRONG_ANSWER, + message=f"Wrong answer.\n{error_message}", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=execution_time_host, + memory_usage=0, + ) + stderr_last_line = stderr.splitlines()[-1] + score_match = re.match(r"Score = (\d+)", stderr_last_line) + if score_match is None: + return CaseResult( + input_str=input_str, + output_str=output_str, + error_str=error_str, + judge_result=JudgeResult.WRONG_ANSWER, + message=f"Wrong answer.\nStandard error:\n{stderr}", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=execution_time_host, + memory_usage=0, + ) + return int(score_match.group(1)) + + def run_reactive_judge_container( code_language: CodeLanguage, judge_version: JudgeVersion, @@ -810,6 +926,54 @@ def run_reactive_judge_container( return (execution_time_host, score, stderr) # Run succeeded, return the execution time +def run_reactive_judge_reusable_container( + reusable_submission_container_pool: ReusableSubmissionContainerPool, + time_limit: float, + judge_command: str, + input_str: str | None, + output_file_path: Path | None, +) -> CaseResult | tuple[float, int, str]: + """Run the reactive judge command in a reusable Docker container.""" + execution_time_host, exit_code, stderr = reusable_submission_container_pool.run(judge_command) + if exit_code != 0 or stderr == "": + if execution_time_host > time_limit: # Killed by `timeout` command + return CaseResult( + input_str=input_str, + output_str=None, + error_str=stderr if input_str is not None else None, + judge_result=JudgeResult.TIME_LIMIT_EXCEEDED, + message="Time limit exceeded.", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=min(execution_time_host, time_limit + 0.1), # NOTE: slight longer than time limit + memory_usage=0, + ) + return CaseResult( + input_str=input_str, + output_str=None, + error_str=stderr if input_str is not None else None, + judge_result=JudgeResult.RUNTIME_ERROR, + message="Runtime error.", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=execution_time_host, + memory_usage=0, + ) + stderr_last_line = stderr.splitlines()[-1] + score_match = re.match(r"Score = (\d+)", stderr_last_line) + if score_match is None: + return CaseResult( + input_str=input_str, + output_str=output_file_path.read_text() if output_file_path else None, + error_str=stderr if input_str is not None else None, + judge_result=JudgeResult.WRONG_ANSWER, + message="Wrong answer.", # NOTE: exclude stderr because we don't want to be exploited by the user + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=execution_time_host, + memory_usage=0, + ) + score = int(score_match.group(1)) + return (execution_time_host, score, stderr) + + def run_vis_container(vis_command: str, vis_volumes: dict[str, dict[str, str]]) -> None: """Run the visualization command in a Docker container. @@ -848,6 +1012,28 @@ def run_vis_container(vis_command: str, vis_volumes: dict[str, dict[str, str]]) raise RuntimeError(msg) +def run_vis_reusable_container( + reusable_tool_container_pool: ReusableToolContainerPool, + vis_command: str, + local_visualization_file: str, + generated_file_path: str, +) -> None: + """Run the visualization command in a reusable Docker tool container.""" + inner_command = ( + f"rm -f {shlex.quote(generated_file_path)}; " + f"{vis_command}; " + f"cp {shlex.quote(generated_file_path)} {shlex.quote(local_visualization_file)}" + ) + timed_command = f"timeout {ale_bench.constants.VISUALIZE_TIMEOUT} bash -c {shlex.quote(inner_command)}" + _execution_time_host, exit_code, _stderr = reusable_tool_container_pool.run(timed_command) + if exit_code == TIMEOUT_EXIT_CODE: + msg = "Timeout while running the visualization command. Something went wrong." + raise RuntimeError(msg) + if exit_code != 0: + msg = "Failed to run the visualization command. Something went wrong." + raise RuntimeError(msg) + + def parse_profiles( time_limit: float, memory_limit: int, @@ -1003,19 +1189,42 @@ def case_iter_func( batch_judge_command: str, reactive_judge_command: str, vis_command: str, + reusable_submission_container_pool: ReusableSubmissionContainerPool | None = None, + reusable_tool_container_pool: ReusableToolContainerPool | None = None, ) -> CaseResult: """Run a single case end-to-end and return its judge result.""" result_input_str = input_str if return_details else None host_paths_judge: HostPathsBatchJudge | HostPathsReactiveJudge execution_time_host = -1.0 + case_temp_dir = ( + reusable_submission_container_pool.scratch_dir if reusable_submission_container_pool is not None else temp_dir + ) if problem_type == ProblemType.BATCH: # Run the submission code and generate the output file - host_paths_run = setup_paths_batch_run(host_paths_compile, temp_dir, input_str, f"{problem_id}_{case_idx:06d}_") - run_volumes = get_batch_run_volumes(host_paths_run, temp_dir) - run_result = run_batch_run_container( - code_language, judge_version, time_limit, run_volumes, batch_run_command, result_input_str + host_paths_run = setup_paths_batch_run( + host_paths_compile, case_temp_dir, input_str, f"{problem_id}_{case_idx:06d}_" ) + if reusable_submission_container_pool is None: + run_volumes = get_batch_run_volumes(host_paths_run, temp_dir) + run_result = run_batch_run_container( + code_language, judge_version, time_limit, run_volumes, batch_run_command, result_input_str + ) + else: + reusable_batch_run_command = build_batch_run_command( + code_language, + judge_version, + time_limit, + input_file=reusable_submission_container_pool.container_path(host_paths_run.input_file), + output_file=reusable_submission_container_pool.container_path(host_paths_run.output_file), + profiles_file=reusable_submission_container_pool.container_path(host_paths_run.profiles_file), + ) + run_result = run_batch_run_reusable_container( + reusable_submission_container_pool, + time_limit, + reusable_batch_run_command, + result_input_str, + ) if isinstance(run_result, CaseResult): return run_result if not isinstance(run_result, tuple): @@ -1043,15 +1252,29 @@ def case_iter_func( execution_time, memory_usage = profiles_result # Calculate score by the input and output files host_paths_judge = setup_paths_batch_judge(host_paths_run) - judge_volumes = get_batch_judge_volumes(host_paths_judge, tool_dir) - batch_judge_result = run_batch_judge_container( - judge_volumes, - batch_judge_command, - execution_time_host, - result_input_str, - result_output_str, - result_error_str, - ) + if reusable_tool_container_pool is None: + judge_volumes = get_batch_judge_volumes(host_paths_judge, tool_dir) + batch_judge_result = run_batch_judge_container( + judge_volumes, + batch_judge_command, + execution_time_host, + result_input_str, + result_output_str, + result_error_str, + ) + else: + reusable_batch_judge_command = build_batch_judge_command( + input_file=reusable_tool_container_pool.container_path(host_paths_judge.input_file), + output_file=reusable_tool_container_pool.container_path(host_paths_judge.output_file), + ) + batch_judge_result = run_batch_judge_reusable_container( + reusable_tool_container_pool, + reusable_batch_judge_command, + execution_time_host, + result_input_str, + result_output_str, + result_error_str, + ) if isinstance(batch_judge_result, CaseResult): return batch_judge_result if not isinstance(batch_judge_result, int): @@ -1061,20 +1284,37 @@ def case_iter_func( elif problem_type == ProblemType.REACTIVE: host_paths_judge = setup_paths_reactive_judge( host_paths_compile, - temp_dir, + case_temp_dir, input_str, f"{problem_id}_{case_idx:06d}_", ) - judge_volumes = get_reactive_judge_volumes(host_paths_judge, temp_dir, tool_dir) - reactive_judge_result = run_reactive_judge_container( - code_language, - judge_version, - time_limit, - judge_volumes, - reactive_judge_command, - result_input_str, - host_paths_judge.output_file if return_details else None, - ) + if reusable_submission_container_pool is None: + judge_volumes = get_reactive_judge_volumes(host_paths_judge, temp_dir, tool_dir) + reactive_judge_result = run_reactive_judge_container( + code_language, + judge_version, + time_limit, + judge_volumes, + reactive_judge_command, + result_input_str, + host_paths_judge.output_file if return_details else None, + ) + else: + reusable_reactive_judge_command = build_reactive_judge_command( + code_language, + judge_version, + time_limit, + input_file=reusable_submission_container_pool.container_path(host_paths_judge.input_file), + output_file=reusable_submission_container_pool.container_path(host_paths_judge.output_file), + profiles_file=reusable_submission_container_pool.container_path(host_paths_judge.profiles_file), + ) + reactive_judge_result = run_reactive_judge_reusable_container( + reusable_submission_container_pool, + time_limit, + reusable_reactive_judge_command, + result_input_str, + host_paths_judge.output_file if return_details else None, + ) wo_profile_result = None if isinstance(reactive_judge_result, CaseResult): wo_profile_result = reactive_judge_result @@ -1124,9 +1364,27 @@ def case_iter_func( local_visualization = None if not skip_local_visualization and problem_id not in ale_bench.constants.NO_LOCAL_VIS: # Run the local visualization command in the Docker container - host_paths_vis = setup_paths_vis(host_paths_judge, temp_dir, problem_id, f"{problem_id}_{case_idx:06d}_") - vis_volumes = get_vis_volumes(host_paths_vis, tool_dir) - run_vis_container(vis_command, vis_volumes) + vis_temp_dir = case_temp_dir if reusable_tool_container_pool is not None else temp_dir + host_paths_vis = setup_paths_vis(host_paths_judge, vis_temp_dir, problem_id, f"{problem_id}_{case_idx:06d}_") + if reusable_tool_container_pool is None: + vis_volumes = get_vis_volumes(host_paths_vis, tool_dir) + run_vis_container(vis_command, vis_volumes) + else: + generated_file_path = ( + ale_bench.constants.LOCAL_VIS_SVG + if host_paths_vis.local_visualization_file.suffix == ".svg" + else ale_bench.constants.LOCAL_VIS_HTML + ) + reusable_vis_command = build_vis_command( + input_file=reusable_tool_container_pool.container_path(host_paths_vis.input_file), + output_file=reusable_tool_container_pool.container_path(host_paths_vis.output_file), + ) + run_vis_reusable_container( + reusable_tool_container_pool, + reusable_vis_command, + reusable_tool_container_pool.container_path(host_paths_vis.local_visualization_file), + generated_file_path, + ) # Read the local visualization SVG or HTML svg_text = host_paths_vis.local_visualization_file.read_text() svg_text = svg_text.replace("\n", "").removeprefix("
").removesuffix("") @@ -1161,6 +1419,7 @@ def run_cases( return_details: bool, skip_local_visualization: bool, num_workers: int, + reuse_containers: bool = False, ) -> list[CaseResult]: """Run the cases for the given inputs and code. @@ -1177,6 +1436,7 @@ def run_cases( return_details (bool): Whether to return detailed results (input_str, output_str, error_str). skip_local_visualization (bool): Whether to skip local visualization. num_workers (int): The number of workers for running cases. + reuse_containers (bool): Whether to reuse long-lived execution containers. Returns: list[CaseResult]: The list of case results. @@ -1210,49 +1470,41 @@ def run_cases( # Run the code and calculate the score in the Docker container case_results: list[CaseResult] = [] - if len(inputs) == 1 or num_workers == 1: - for case_idx, input_str in enumerate(inputs): - case_result = case_iter_func( - problem_id, - time_limit, - memory_limit, - problem_type, - case_idx, - input_str, - code_language, - judge_version, - temp_dir, - tool_dir, - return_details, - skip_local_visualization, - host_paths_compile, - batch_run_command, - batch_judge_command, - reactive_judge_command, - vis_command, + reusable_scratch_dir = temp_dir / "reusable_case_files" + use_reusable_containers = reuse_containers and bool(inputs) + if use_reusable_containers: + reusable_scratch_dir.mkdir() + reusable_submission_pool_context = ReusableSubmissionContainerPool( + code_language=code_language, + judge_version=judge_version, + temp_dir=temp_dir, + scratch_dir=reusable_scratch_dir, + tool_dir=tool_dir, + problem_type=problem_type, + num_workers=max(1, min(num_workers, len(inputs))), + ) + use_reusable_tool_pool = problem_type == ProblemType.BATCH or ( + not skip_local_visualization and problem_id not in ale_bench.constants.NO_LOCAL_VIS + ) + if use_reusable_tool_pool: + reusable_tool_pool_context = ReusableToolContainerPool( + scratch_dir=reusable_scratch_dir, + tool_dir=tool_dir, + num_workers=max(1, min(num_workers, len(inputs))), ) - # Add the result - case_results.append(case_result) + else: + reusable_tool_pool_context = nullcontext(None) else: - case_results = [ - CaseResult( - input_str=input_str if return_details else None, - output_str=None, - error_str=None, - judge_result=JudgeResult.INTERNAL_ERROR, - message="Internal Error: Unexpected error occurred.", - absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, - execution_time=0.0, - memory_usage=0, - ) - for input_str in inputs - ] - # Use ThreadPoolExecutor to run the cases in parallel - with ThreadPoolExecutor(max_workers=num_workers) as executor: - future_to_case_idx = {} + reusable_submission_pool_context = nullcontext(None) + reusable_tool_pool_context = nullcontext(None) + + with ( + reusable_submission_pool_context as reusable_submission_container_pool, + reusable_tool_pool_context as reusable_tool_container_pool, + ): + if len(inputs) == 1 or num_workers == 1: for case_idx, input_str in enumerate(inputs): - future = executor.submit( - case_iter_func, + case_result = case_iter_func( problem_id, time_limit, memory_limit, @@ -1270,23 +1522,67 @@ def run_cases( batch_judge_command, reactive_judge_command, vis_command, + reusable_submission_container_pool, + reusable_tool_container_pool, + ) + # Add the result + case_results.append(case_result) + else: + case_results = [ + CaseResult( + input_str=input_str if return_details else None, + output_str=None, + error_str=None, + judge_result=JudgeResult.INTERNAL_ERROR, + message="Internal Error: Unexpected error occurred.", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=0.0, + memory_usage=0, ) - future_to_case_idx[future] = case_idx - for future in as_completed(future_to_case_idx): - case_idx = future_to_case_idx[future] - try: - case_result = future.result() - except Exception as e: - case_result = CaseResult( - input_str=inputs[case_idx] if return_details else None, - output_str=None, - error_str=None, - judge_result=JudgeResult.INTERNAL_ERROR, - message=f"Internal Error: {e}", - absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, - execution_time=0.0, - memory_usage=0, + for input_str in inputs + ] + # Use ThreadPoolExecutor to run the cases in parallel + with ThreadPoolExecutor(max_workers=num_workers) as executor: + future_to_case_idx = {} + for case_idx, input_str in enumerate(inputs): + future = executor.submit( + case_iter_func, + problem_id, + time_limit, + memory_limit, + problem_type, + case_idx, + input_str, + code_language, + judge_version, + temp_dir, + tool_dir, + return_details, + skip_local_visualization, + host_paths_compile, + batch_run_command, + batch_judge_command, + reactive_judge_command, + vis_command, + reusable_submission_container_pool, + reusable_tool_container_pool, ) - case_results[case_idx] = case_result + future_to_case_idx[future] = case_idx + for future in as_completed(future_to_case_idx): + case_idx = future_to_case_idx[future] + try: + case_result = future.result() + except Exception as e: + case_result = CaseResult( + input_str=inputs[case_idx] if return_details else None, + output_str=None, + error_str=None, + judge_result=JudgeResult.INTERNAL_ERROR, + message=f"Internal Error: {e}", + absolute_score=ale_bench.constants.REJECTED_ABSOLUTE_SCORE, + execution_time=0.0, + memory_usage=0, + ) + case_results[case_idx] = case_result return case_results diff --git a/src/ale_bench/tool_wrappers/reusable_container_pool.py b/src/ale_bench/tool_wrappers/reusable_container_pool.py new file mode 100644 index 00000000..20eb2392 --- /dev/null +++ b/src/ale_bench/tool_wrappers/reusable_container_pool.py @@ -0,0 +1,309 @@ +"""Reusable Docker container pools for case execution.""" + +from __future__ import annotations + +import os +import time +from contextlib import suppress +from queue import Queue +from typing import TYPE_CHECKING + +import ale_bench.constants +from ale_bench.code_language import CodeLanguage, JudgeVersion, get_docker_image_name +from ale_bench.data import ProblemType +from ale_bench.utils import docker_client + +if TYPE_CHECKING: + from pathlib import Path + from types import TracebackType + + from docker import DockerClient + from docker.models.containers import Container + +REUSABLE_SUBMISSION_TMP_DIR = f"{ale_bench.constants.TMP_DIR}/ale-bench-run" +REUSABLE_TOOL_TMP_DIR = f"{ale_bench.constants.TMP_DIR}/ale-bench-tool" + + +def get_reusable_tool_volumes( + scratch_dir: Path, + tool_dir: Path, +) -> dict[str, dict[str, str]]: + """Get volumes for a reusable judge/visualization tool container.""" + return { + str(scratch_dir): {"bind": REUSABLE_TOOL_TMP_DIR, "mode": "rw"}, + str(tool_dir / "tools" / "target" / "release" / "tester"): { + "bind": ale_bench.constants.TESTER_BIN, + "mode": "ro", + }, + str(tool_dir / "tools" / "target" / "release" / "vis"): { + "bind": ale_bench.constants.VIS_BIN, + "mode": "ro", + }, + } + + +def get_reusable_submission_volumes( + temp_dir: Path, + scratch_dir: Path, + tool_dir: Path, + problem_type: ProblemType, +) -> dict[str, dict[str, str]]: + """Get volumes for a reusable submission container. + + The compiled submission directory is mounted read-only at WORK_DIR. Per-case input, + output, and profile files live in a separate read-write scratch mount. + """ + volumes = { + str(temp_dir): {"bind": ale_bench.constants.WORK_DIR, "mode": "ro"}, + str(scratch_dir): {"bind": REUSABLE_SUBMISSION_TMP_DIR, "mode": "rw"}, + } + if problem_type == ProblemType.REACTIVE: + volumes[str(tool_dir / "tools" / "target" / "release" / "tester")] = { + "bind": ale_bench.constants.TESTER_BIN, + "mode": "ro", + } + return volumes + + +class ReusableSubmissionContainerPool: + """A fixed-size pool of long-lived submission execution containers.""" + + def __init__( + self, + code_language: CodeLanguage, + judge_version: JudgeVersion, + temp_dir: Path, + scratch_dir: Path, + tool_dir: Path, + problem_type: ProblemType, + num_workers: int, + ) -> None: + """Initialize the reusable container pool configuration.""" + self.code_language = code_language + self.judge_version = judge_version + self.temp_dir = temp_dir + self.scratch_dir = scratch_dir + self.tool_dir = tool_dir + self.problem_type = problem_type + self.num_workers = num_workers + self._client_context = docker_client() + self._client: DockerClient | None = None + self._containers: list[Container] = [] + self._available_containers: Queue[Container] = Queue() + + def __enter__(self) -> ReusableSubmissionContainerPool: # noqa: PYI034 + """Create and start all worker containers.""" + self._client = self._client_context.__enter__() + for _ in range(self.num_workers): + self._available_containers.put(self._create_container()) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Remove all worker containers and close the Docker client.""" + for container in self._containers: + self._remove_container(container) + self._containers.clear() + self._client_context.__exit__(exc_type, exc_value, traceback) + self._client = None + + def container_path(self, host_path: Path) -> str: + """Convert a scratch host path to its reusable-container path.""" + return f"{REUSABLE_SUBMISSION_TMP_DIR}/{host_path.relative_to(self.scratch_dir)}" + + def run(self, command: str) -> tuple[float, int, str]: + """Run one command via docker exec in an available reusable container.""" + container = self._available_containers.get() + should_replace = False + try: + start_at = time.perf_counter() + exec_result = container.exec_run( + cmd=["/bin/bash", "--noprofile", "--norc", "-c", command], + demux=True, + stderr=True, + stdout=True, + user=str(os.getuid()), + workdir=ale_bench.constants.WORK_DIR, + ) + end_at = time.perf_counter() + _stdout_bytes, stderr_bytes = exec_result.output or (b"", b"") + stderr = (stderr_bytes or b"").decode("utf-8", errors="replace").strip() + return end_at - start_at, exec_result.exit_code, stderr + except Exception: + should_replace = True + raise + finally: + self._release_container(container, should_replace) + + def _create_container(self) -> Container: + if self._client is None: + msg = "ReusableSubmissionContainerPool must be entered before creating containers." + raise RuntimeError(msg) + volumes = get_reusable_submission_volumes( + self.temp_dir, + self.scratch_dir, + self.tool_dir, + self.problem_type, + ) + container = self._client.containers.run( + image=get_docker_image_name(self.code_language, self.judge_version), + command=["/bin/bash", "--noprofile", "--norc", "-c", "sleep infinity"], + remove=False, + auto_remove=False, + cpu_period=100000, + cpu_quota=100000, # 1 CPU + detach=True, + group_add=[os.getgid()], + mem_limit=ale_bench.constants.MAX_MEMORY_LIMIT, + network_disabled=True, + user=os.getuid(), + volumes=volumes, + working_dir=ale_bench.constants.WORK_DIR, + ) + self._containers.append(container) + return container + + def _release_container(self, container: Container, should_replace: bool) -> None: + if should_replace or not self._is_container_running(container): + self._remove_container(container) + if container in self._containers: + self._containers.remove(container) + self._available_containers.put(self._create_container()) + else: + self._available_containers.put(container) + + def _is_container_running(self, container: Container) -> bool: + try: + container.reload() + attrs = container.attrs + if not isinstance(attrs, dict): + return False + state = attrs.get("State", {}) + if not isinstance(state, dict): + return False + return bool(state.get("Running", False)) + except Exception: + return False + + def _remove_container(self, container: Container) -> None: + with suppress(Exception): + container.remove(force=True) + + +class ReusableToolContainerPool: + """A fixed-size pool of long-lived judge/visualization tool containers.""" + + def __init__( + self, + scratch_dir: Path, + tool_dir: Path, + num_workers: int, + ) -> None: + """Initialize the reusable tool container pool configuration.""" + self.scratch_dir = scratch_dir + self.tool_dir = tool_dir + self.num_workers = num_workers + self._client_context = docker_client() + self._client: DockerClient | None = None + self._containers: list[Container] = [] + self._available_containers: Queue[Container] = Queue() + + def __enter__(self) -> ReusableToolContainerPool: # noqa: PYI034 + """Create and start all worker containers.""" + self._client = self._client_context.__enter__() + for _ in range(self.num_workers): + self._available_containers.put(self._create_container()) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Remove all worker containers and close the Docker client.""" + for container in self._containers: + self._remove_container(container) + self._containers.clear() + self._client_context.__exit__(exc_type, exc_value, traceback) + self._client = None + + def container_path(self, host_path: Path) -> str: + """Convert a scratch host path to its reusable-container path.""" + return f"{REUSABLE_TOOL_TMP_DIR}/{host_path.relative_to(self.scratch_dir)}" + + def run(self, command: str, *, workdir: str = ale_bench.constants.WORK_DIR) -> tuple[float, int, str]: + """Run one command via docker exec in an available reusable tool container.""" + container = self._available_containers.get() + should_replace = False + try: + start_at = time.perf_counter() + exec_result = container.exec_run( + cmd=["/bin/bash", "--noprofile", "--norc", "-c", command], + demux=True, + stderr=True, + stdout=True, + user=str(os.getuid()), + workdir=workdir, + ) + end_at = time.perf_counter() + _stdout_bytes, stderr_bytes = exec_result.output or (b"", b"") + stderr = (stderr_bytes or b"").decode("utf-8", errors="replace").strip() + return end_at - start_at, exec_result.exit_code, stderr + except Exception: + should_replace = True + raise + finally: + self._release_container(container, should_replace) + + def _create_container(self) -> Container: + if self._client is None: + msg = "ReusableToolContainerPool must be entered before creating containers." + raise RuntimeError(msg) + container = self._client.containers.run( + image=ale_bench.constants.RUST_TOOL_DOCKER_IMAGE, + command=["/bin/bash", "--noprofile", "--norc", "-c", "sleep infinity"], + remove=False, + auto_remove=False, + cpu_period=100000, + cpu_quota=100000, # 1 CPU + detach=True, + group_add=[os.getgid()], + mem_limit=ale_bench.constants.MAX_MEMORY_LIMIT, + network_disabled=True, + user=os.getuid(), + volumes=get_reusable_tool_volumes(self.scratch_dir, self.tool_dir), + working_dir=ale_bench.constants.WORK_DIR, + ) + self._containers.append(container) + return container + + def _release_container(self, container: Container, should_replace: bool) -> None: + if should_replace or not self._is_container_running(container): + self._remove_container(container) + if container in self._containers: + self._containers.remove(container) + self._available_containers.put(self._create_container()) + else: + self._available_containers.put(container) + + def _is_container_running(self, container: Container) -> bool: + try: + container.reload() + attrs = container.attrs + if not isinstance(attrs, dict): + return False + state = attrs.get("State", {}) + if not isinstance(state, dict): + return False + return bool(state.get("Running", False)) + except Exception: + return False + + def _remove_container(self, container: Container) -> None: + with suppress(Exception): + container.remove(force=True) diff --git a/src/ale_bench_eval/__main__.py b/src/ale_bench_eval/__main__.py index 7a40fd94..3327dd07 100644 --- a/src/ale_bench_eval/__main__.py +++ b/src/ale_bench_eval/__main__.py @@ -68,6 +68,7 @@ def evaluate_contest( problem_id: str, lite_version: bool, num_workers: int, + reuse_containers: bool, n_public_cases: int | None = None, selection_method: Literal["best", "median"] = "median", root_path: Path | None = None, @@ -83,6 +84,7 @@ def evaluate_contest( n_repeated_sampling=n_repeated_sampling, n_self_refine=n_self_refine, num_workers=num_workers, + reuse_containers=reuse_containers, n_public_cases=n_public_cases, prompt_args=prompt_args, problem_id=problem_id, @@ -249,6 +251,7 @@ def _run_evaluation_task( problem_id: str, lite_version: bool, num_workers: int, + reuse_containers: bool, n_public_cases: int | None, selection_method: Literal["best", "median"], root_path: Path, @@ -267,6 +270,7 @@ def _run_evaluation_task( problem_id=problem_id, lite_version=lite_version, num_workers=num_workers, + reuse_containers=reuse_containers, n_public_cases=n_public_cases, selection_method=selection_method, root_path=root_path, @@ -287,6 +291,7 @@ def main( n_repeated_sampling: int = 1, n_self_refine: int = 1, num_workers: int = 1, + reuse_containers: bool = False, n_public_cases: int | None = None, code_language: EvalCodeLanguage = "cpp20", judge_version: EvalJudgeVersion = "202301", @@ -383,6 +388,9 @@ def main( # NOTE: skip num_workers check to allow resuming with different num_workers # NOTE: skip max_concurrent_llm_calls check to allow resuming with different LLM concurrency # NOTE: skip max_repeated_sampling_workers check to allow resuming with different repeated sampling concurrency + if existing_settings.get("reuse_containers", False) != reuse_containers: + msg = "Experiment settings already exist with different reuse_containers" + raise ValueError(msg) if existing_settings["n_public_cases"] != n_public_cases: msg = "Experiment settings already exist with different n_public_cases" raise ValueError(msg) @@ -411,6 +419,7 @@ def main( "n_repeated_sampling": n_repeated_sampling, "n_self_refine": n_self_refine, "num_workers": num_workers, + "reuse_containers": reuse_containers, "n_public_cases": n_public_cases, "code_language": code_language, "judge_version": judge_version, @@ -432,6 +441,7 @@ def main( print( f"📊 Model: {model_name}, Repeated Sampling: {n_repeated_sampling}, Self-Refine: {n_self_refine}, " f"Code Language: {code_language}, Judge Version: {judge_version}, " + f"Reuse Containers: {reuse_containers}, " f"Max Concurrent LLM Calls: {max_concurrent_llm_calls}, " f"Max Repeated Sampling Workers: {max_repeated_sampling_workers}" ) @@ -459,6 +469,7 @@ def main( problem_id, lite_version, num_workers, + reuse_containers, n_public_cases, selection_method, exp_root, diff --git a/src/ale_bench_eval/calc_cost.py b/src/ale_bench_eval/calc_cost.py index 57a276e4..4440045b 100644 --- a/src/ale_bench_eval/calc_cost.py +++ b/src/ale_bench_eval/calc_cost.py @@ -141,6 +141,12 @@ cache_read_mtok=Decimal(5) / Decimal(10), output_mtok=Decimal(25), ), + "claude-fable-5": ModelPrice( + input_mtok=Decimal(10), + cache_write_mtok=Decimal(125) / Decimal(10), + cache_read_mtok=Decimal(1), + output_mtok=Decimal(50), + ), "grok-4.1-fast": ModelPrice( input_mtok=Decimal(2) / Decimal(10), cache_read_mtok=Decimal(5) / Decimal(100), @@ -244,6 +250,11 @@ output_mtok=Decimal(44) / Decimal(10), cache_read_mtok=Decimal(26) / Decimal(100), ), + "glm-5.2": ModelPrice( + input_mtok=Decimal(14) / Decimal(10), + output_mtok=Decimal(44) / Decimal(10), + cache_read_mtok=Decimal(26) / Decimal(100), + ), "glm-5-turbo": ModelPrice( input_mtok=Decimal(12) / Decimal(10), output_mtok=Decimal(4), @@ -283,6 +294,11 @@ output_mtok=Decimal(4), cache_read_mtok=Decimal(16) / Decimal(100), ), + "kimi-k2.7-code": ModelPrice( + input_mtok=Decimal(95) / Decimal(100), + output_mtok=Decimal(4), + cache_read_mtok=Decimal(19) / Decimal(100), + ), "mercury-2": ModelPrice( input_mtok=Decimal(25) / Decimal(100), cache_read_mtok=Decimal(25) / Decimal(1000), @@ -314,6 +330,16 @@ output_mtok=Decimal(5) / Decimal(10), cache_read_mtok=Decimal(1) / Decimal(10), ), + "ling-2.6-1t": ModelPrice( + input_mtok=Decimal(3) / Decimal(10), + cache_read_mtok=Decimal(6) / Decimal(100), + output_mtok=Decimal(25) / Decimal(10), + ), + "ring-2.6-1t": ModelPrice( + input_mtok=Decimal(3) / Decimal(10), + cache_read_mtok=Decimal(6) / Decimal(100), + output_mtok=Decimal(25) / Decimal(10), + ), "qwen3-235b-a22b-thinking-2507": ModelPrice( input_mtok=Decimal(3) / Decimal(10), output_mtok=Decimal(29) / Decimal(10) ), diff --git a/src/ale_bench_eval/data_types.py b/src/ale_bench_eval/data_types.py index a4f7246b..1bb0c804 100644 --- a/src/ale_bench_eval/data_types.py +++ b/src/ale_bench_eval/data_types.py @@ -21,6 +21,7 @@ class EvaluationConfig: n_repeated_sampling: int n_self_refine: int num_workers: int + reuse_containers: bool n_public_cases: int | None prompt_args: PromptArgs problem_id: str diff --git a/src/ale_bench_eval/evaluate.py b/src/ale_bench_eval/evaluate.py index 276b4d94..3a2e45fb 100644 --- a/src/ale_bench_eval/evaluate.py +++ b/src/ale_bench_eval/evaluate.py @@ -112,6 +112,7 @@ def run_private_evaluation( solution_code, code_language=solution_code_language, judge_version=config.prompt_args.judge_version, + reuse_containers=config.reuse_containers, ) if save_info is not None: diff --git a/src/ale_bench_eval/scaffolds.py b/src/ale_bench_eval/scaffolds.py index b702071b..10128905 100644 --- a/src/ale_bench_eval/scaffolds.py +++ b/src/ale_bench_eval/scaffolds.py @@ -72,12 +72,14 @@ def _evaluate_public_result_once( code_language, judge_version=config.prompt_args.judge_version, skip_local_visualization=True, + reuse_containers=config.reuse_containers, ) else: public_result = session.public_eval( code, code_language, judge_version=config.prompt_args.judge_version, + reuse_containers=config.reuse_containers, ) if public_result.overall_judge_result == JudgeResult.INTERNAL_ERROR: msg = "Judge returned INTERNAL_ERROR." diff --git a/tests/judge/codes/ac_cpp20_ahc001.cpp b/tests/judge/codes/ac_cpp20_ahc001.cpp new file mode 100644 index 00000000..20210543 --- /dev/null +++ b/tests/judge/codes/ac_cpp20_ahc001.cpp @@ -0,0 +1,2468 @@ +#include, greater
> pq;
+
+ dist[S] = 0.0;
+ pre[S] = S;
+ pq.push({0.0, S});
+
+ while (!pq.empty()) {
+ auto [d, u] = pq.top();
+ pq.pop();
+
+ if (d > dist[u] + 1e-9) continue;
+ if (u == T) break;
+
+ int r = u / N;
+ int c = u % N;
+
+ int dirs[4];
+ bool used[4] = {};
+ int m = 0;
+
+ auto addDir = [&](int x) {
+ if (!used[x]) {
+ used[x] = true;
+ dirs[m++] = x;
+ }
+ };
+
+ if (ti < r) addDir(0);
+ if (ti > r) addDir(1);
+ if (tj < c) addDir(2);
+ if (tj > c) addDir(3);
+ for (int x = 0; x < 4; x++) addDir(x);
+
+ for (int idx = 0; idx < 4; idx++) {
+ int dir = dirs[idx];
+
+ int nr = r, nc = c;
+ char mv = '?';
+ double w = 0.0;
+
+ if (dir == 0) {
+ if (r == 0) continue;
+ nr = r - 1;
+ mv = 'U';
+ if (!useFine) w = vCostNoFine(r - 1, c);
+ else if (!useEdge) w = vCostNoEdge(r - 1, c);
+ else w = vCost(r - 1, c);
+ } else if (dir == 1) {
+ if (r == N - 1) continue;
+ nr = r + 1;
+ mv = 'D';
+ if (!useFine) w = vCostNoFine(r, c);
+ else if (!useEdge) w = vCostNoEdge(r, c);
+ else w = vCost(r, c);
+ } else if (dir == 2) {
+ if (c == 0) continue;
+ nc = c - 1;
+ mv = 'L';
+ if (!useFine) w = hCostNoFine(r, c - 1);
+ else if (!useEdge) w = hCostNoEdge(r, c - 1);
+ else w = hCost(r, c - 1);
+ } else {
+ if (c == N - 1) continue;
+ nc = c + 1;
+ mv = 'R';
+ if (!useFine) w = hCostNoFine(r, c);
+ else if (!useEdge) w = hCostNoEdge(r, c);
+ else w = hCost(r, c);
+ }
+
+ int v = nr * N + nc;
+ double nd = d + w;
+
+ if (nd + 1e-9 < dist[v]) {
+ dist[v] = nd;
+ pre[v] = u;
+ pmove[v] = mv;
+ pq.push({nd, v});
+ }
+ }
+ }
+
+ if (pre[T] == -1) return "";
+
+ string path;
+ int cur = T;
+ while (cur != S) {
+ path.push_back(pmove[cur]);
+ cur = pre[cur];
+ if (cur < 0) return "";
+ }
+
+ reverse(path.begin(), path.end());
+ return path;
+ }
+
+ string choosePath(int si, int sj, int ti, int tj) {
+ if (turn < EXPLORE_TURNS) {
+ return chooseExplorationPath(si, sj, ti, tj);
+ }
+
+ string safe = bestCorridorPath(si, sj, ti, tj);
+ string p = dijkstra(si, sj, ti, tj, true, true);
+
+ if (p.empty() || !validatePath(si, sj, ti, tj, p)) {
+ return safe;
+ }
+
+ if (turn >= 620 && edgeWeight() > 1e-9) {
+ string pne = dijkstra(si, sj, ti, tj, true, false);
+
+ if (!pne.empty() && validatePath(si, sj, ti, tj, pne)) {
+ double cfP = estimatePathCost(si, sj, p);
+ double ceP = estimatePathCostNoEdge(si, sj, p);
+ double cfN = estimatePathCost(si, sj, pne);
+ double ceN = estimatePathCostNoEdge(si, sj, pne);
+
+ double trustEdge = clampd(0.65 + 0.25 * edgeValidationConf, 0.70, 0.90);
+ double scoreP = trustEdge * cfP + (1.0 - trustEdge) * ceP;
+ double scoreN = trustEdge * cfN + (1.0 - trustEdge) * ceN;
+
+ if (scoreN < scoreP * 0.998 && (int)pne.size() <= (int)p.size() + 35) {
+ p = pne;
+ }
+ }
+ }
+
+ if (fineReady && turn >= 380) {
+ string pn = dijkstra(si, sj, ti, tj, false, false);
+
+ if (!pn.empty() && validatePath(si, sj, ti, tj, pn)) {
+ double cfP = estimatePathCost(si, sj, p);
+ double csP = estimatePathCostNoFine(si, sj, p);
+ double cfN = estimatePathCost(si, sj, pn);
+ double csN = estimatePathCostNoFine(si, sj, pn);
+
+ double trustFine = clampd(0.70 + 0.22 * fineValidationConf, 0.80, 0.92);
+
+ double scoreP = trustFine * cfP + (1.0 - trustFine) * csP;
+ double scoreN = trustFine * cfN + (1.0 - trustFine) * csN;
+
+ if (scoreN < scoreP * 0.997 && (int)pn.size() <= (int)p.size() + 40) {
+ p = pn;
+ }
+ }
+ }
+
+ int manhattan = abs(si - ti) + abs(sj - tj);
+
+ int extraLimit;
+ if (turn < 200) extraLimit = 12;
+ else if (turn < 350) extraLimit = 25;
+ else if (turn < 500) extraLimit = 45;
+ else if (turn < 700) extraLimit = 65;
+ else extraLimit = 80 + int(20.0 * segGlobalConf);
+
+ double cd = estimatePathCost(si, sj, p);
+ double cs = estimatePathCost(si, sj, safe);
+
+ int extra = int(p.size()) - manhattan;
+
+ if (extra > extraLimit) {
+ if ((int)p.size() > manhattan + 130) return safe;
+ if (cd > cs * 0.82) return safe;
+ }
+
+ int lenDiff = int(p.size()) - int(safe.size());
+ if (lenDiff > 30) {
+ double requiredRatio = 1.0 - min(0.08, 0.001 * lenDiff);
+ if (cd > cs * requiredRatio) return safe;
+ }
+
+ return p;
+ }
+
+ void updateFineValidation(int si, int sj, const string& path, int result) {
+ if (!fineReady || turn < 350 || result <= 0) return;
+
+ double ps = estimatePathCostNoFine(si, sj, path);
+ double pf = estimatePathCostNoEdge(si, sj, path);
+
+ double denom = max(1.0, double(result));
+ double es = (ps - result) / denom;
+ double ef = (pf - result) / denom;
+
+ double es2 = min(0.25, es * es);
+ double ef2 = min(0.25, ef * ef);
+
+ fineValidationWindow.push_back({es2, ef2});
+ if ((int)fineValidationWindow.size() > 220) fineValidationWindow.pop_front();
+
+ if ((int)fineValidationWindow.size() < 120) {
+ fineValidationConf = 1.0;
+ return;
+ }
+
+ double ss = 0.0;
+ double sf = 0.0;
+ for (auto [a, b] : fineValidationWindow) {
+ ss += a;
+ sf += b;
+ }
+
+ ss /= fineValidationWindow.size();
+ sf /= fineValidationWindow.size();
+
+ double worse = sf - ss;
+
+ if (worse <= 0.0008) {
+ fineValidationConf = 1.0;
+ } else {
+ fineValidationConf = clampd(1.0 - (worse - 0.0008) / 0.0030 * 0.45, 0.55, 1.0);
+ }
+ }
+
+ void updateEdgeValidation(int si, int sj, const string& path, int result) {
+ if (turn < 600 || result <= 0) return;
+
+ double ps = estimatePathCostNoEdge(si, sj, path);
+ double pe = estimatePathCost(si, sj, path);
+
+ double denom = max(1.0, double(result));
+ double es = (ps - result) / denom;
+ double ee = (pe - result) / denom;
+
+ double es2 = min(0.25, es * es);
+ double ee2 = min(0.25, ee * ee);
+
+ edgeValidationWindow.push_back({es2, ee2});
+ if ((int)edgeValidationWindow.size() > 180) edgeValidationWindow.pop_front();
+
+ if ((int)edgeValidationWindow.size() < 90) {
+ edgeValidationConf = 1.0;
+ return;
+ }
+
+ double ss = 0.0;
+ double se = 0.0;
+ for (auto [a, b] : edgeValidationWindow) {
+ ss += a;
+ se += b;
+ }
+
+ ss /= edgeValidationWindow.size();
+ se /= edgeValidationWindow.size();
+
+ double worse = se - ss;
+
+ if (worse <= 0.0006) {
+ edgeValidationConf = 1.0;
+ } else {
+ edgeValidationConf = clampd(1.0 - (worse - 0.0006) / 0.0025 * 0.50, 0.50, 1.0);
+ }
+ }
+
+ void updateEdgeResidual(int si, int sj, const string& path, int result, const vector