Repository navigation
[GDPVal] Align the sandbox and prompt to GDPval-AA v2 - #3333
Conversation
Mirrors the eval-factory refs.yaml change. The nine-ref ladder stopped at 1223 while the AA GDPval-AA v2 board runs to 1764, so a frontier candidate had no anchor above it and its elo came out of a saturated win rate rather than out of the fit. deepseek_v4_flash_0731 1468 new ceiling inkling_small 1191 qwen35_122b 925 closes the 905-992 gap Elos are the AA rows read 2026-09-11. Each resolves under GDPVAL_REFS_ROOT like the existing nine, so a local run picks them up by pointing that env var at a directory holding the same per-reference subdirectory names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
Our GDPval container and task prompt still described the GDPval-AA v1
environment, and they disagreed with each other. The prompt advertised
torch 2.10.0, keras, jax, dlib, mtcnn, pygraphviz, pdfkit, imgkit and
fuzzywuzzy; gdpval.def deliberately installed none of them. A model that
believes it has PyTorch plans a PyTorch solution, spends turns finding
out it does not, and submits worse work for reasons unrelated to its
ability.
Vendor the two dependency snapshots Artificial Analysis publishes for
GDPval-AA v2 and build against them:
gdpval_aa_v2_python_requirements.txt 419 pinned Python packages
gdpval_aa_v2_apt_closure.txt 762 pinned apt packages
Sandbox changes:
* Base image moves from python:3.13-bookworm to python:3.13-trixie. The
published apt closure is pinned to Debian 13 (`+deb13uN`, t64 library
names), so a bookworm base shipped systematically older libreoffice,
ffmpeg and chromium than the environment the reference scores came
from.
* Python packages install from the published pin file verbatim instead
of resolving from loose top-level names, so the image reproduces the
reference sandbox rather than drifting to whatever the resolver picks
on build day. A post-install check asserts every pin landed at the
published version, which catches a late pin quietly downgrading an
earlier one.
* Still no PyTorch: torch, torchvision, torchaudio, keras, jax, dlib
and mtcnn are all absent from the published v2 snapshot.
Prompt changes: replace the fabricated version-by-version package listing
with the v2 stack summary, and state the absence of deep learning
frameworks explicitly. Runtime facts are described as our Apptainer
backend actually behaves, not as the published text reads: our backend
feeds commands to one long-lived bash, so shell state does carry over,
and the per-command limit is the harness's 5 minutes rather than E2B's
10. Copying the published wording verbatim would have misinformed the
model on both counts.
Add verify_gdpval_sandbox.py, staged into the image at /opt/gdpval, which
audits a built sif against both snapshots and then exercises the
toolchain end to end, because a tool that is present but cannot produce a
file is the failure that costs a whole run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
The build command in the README no longer works as written: gdpval.def now stages the vendored GDPval-AA v2 manifests with %files, and apptainer resolves those source paths relative to the build working directory, so building from the repository root cannot find them. Say to build from containers/, note that the image is x86_64 only, and document the staged verifier that audits a finished sif against both manifests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
Verifying the first build against the published apt closure surfaced ten behavioural gaps, all of them fonts plus locales and miller. Building with --no-install-recommends drops what LibreOffice only recommends, so the image had no fonts-crosextra-caladea/carlito and none of the Noto families the closure carries. Carlito and Caladea are the metric-compatible substitutes for Calibri and Cambria. Without them LibreOffice silently falls back to a different face, so every Office document written in the Microsoft defaults reflows: page counts and layout drift from what the task author saw, and nothing in the logs says why. Missing Noto means CJK text renders as tofu. Install the fonts, locales and miller explicitly, and check the substitution in %post so a future --no-install-recommends change fails the build instead of quietly degrading deliverable fidelity. The verifier grows a fonts section that asserts the same properties on a finished sif. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
Two gaps remained between the sif and the published GDPval-AA v2 sandbox. The interpreter was wrong. The published apt closure pins python3.13=3.13.5, but basing on the docker python:3.13 image gave 3.13.15, so the eval ran on a different CPython patch release than the environment the reference scores came from. Base on plain debian:trixie and install Debian's python3.13, then assert the micro version in %post so a base image bump cannot slide a different interpreter in unnoticed. pip installs into the system interpreter with --break-system-packages, which is correct for a single-purpose image whose entire point is this dependency set. The manifest was x86_64 only. GDPval also has to run on the two arm64 clusters, and four of the 419 pins publish x86_64 wheels with no source distribution, so they cannot be built there at all: aspose-words, cadquery-ocp, cadquery_vtk and nlopt, plus cadquery itself which declares a hard dependency on cadquery-ocp. That set was derived by querying the PyPI JSON API for every pinned version rather than by trial and error; 406 of the 419 have aarch64 wheels and the remaining 9 build from sdist. arm64 builds now drop exactly that closed list and nothing else. Any other pin failing still aborts the build, and the verifier fails if the image dropped something the list does not cover, so a thinner sandbox cannot pass as a good one. Also close the last two apt gaps the verifier reported, libopenblas0 and libopenblas0-pthread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
The trixie base broke the build at the first pip call. Debian installs pip without a RECORD file, so `pip install --upgrade pip` cannot uninstall the existing copy and exits with uninstall-no-record-file, failing %post before a single pin was installed. Nothing needed the upgrade. Debian's pip resolves the pinned manifest fine, and taking whatever trixie ships removes one more thing that can differ between two builds of the same def. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
The trixie build died partway through the 419-pin install: pip tried to uninstall tomlkit 0.13.2 to put the pinned copy on top and hit uninstall-no-record-file, because that copy is python3-tomlkit from apt and dpkg-managed distributions carry no RECORD file. The overlap is not accidental. gdal-bin and yq pull python3-tomlkit, python3-xmltodict, python3-yaml, python3-numpy and python3-argcomplete, and the published apt closure carries exactly the same packages while the Python manifest pins its own versions of several of them. So install over them with --ignore-installed instead of fighting it. The pinned copy lands in /usr/local/lib/python3/dist-packages, which Debian puts ahead of /usr/lib/python3/dist-packages on sys.path, so the pinned version is the one that imports. That reproduces the reference sandbox's layout, where python3-numpy 2.2.4 and the numpy 2.4.4 pin coexist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
Review catch from Serge Panev on #3333. The prompt told the model that the working directory and environment variables carry over between tool calls. They do not, and a prompt that is wrong about this is expensive: the model `cd`s once and then silently operates in the wrong directory for the rest of the task. I reasoned from the provider keeping one long-lived bash process and never checked how commands are fed into it. There are three independent reasons no state survives, any one of which is on its own fatal to a `cd`: 1. every call is prefixed with `cd <working_dir>`, overwriting any earlier `cd` before the command runs; 2. the command runs inside `bash -c`; 3. which is itself wrapped in a `( ... )` subshell. Files persist, since it is the same filesystem. Directories, exported variables, shell functions and aliases do not. That makes the published Artificial Analysis wording correct for this backend too, so the prompt now matches it. Also his second point: the test asserted the wording rather than the behaviour, so it locked the error in instead of catching it. Extracted `_build_command_script` from `_exec` and assert against the script the provider actually builds. The wording test stays, but only as a pair with the behavioural one -- on its own it proves nothing, since wording can always be made to agree with itself. Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
|
Good catch, and correct — thank you. Fixed in 74b8765; the PR description is updated too, since it advertised the same wrong claim. You were right about the subshell, and there is a stronger reason next to it. Three independent things in script = (
f"cd {self._working_dir} && " # 1. the directory is reset before EVERY call
f"( {wrapped_cmd} ) 2>..." # 3. outer subshell
)
# wrapped_cmd = f"timeout -k 10 {timeout} bash -c {shlex.quote(cmd)}" # 2. inner subshellLine 360 is the one I should have seen: an earlier How I got it wrong: I saw the provider keeps one long-lived bash process and concluded state persists, without reading how commands are fed into it. I then treated the published Artificial Analysis wording as E2B-specific and "corrected" it, when it was accurate for this backend all along. The prompt now matches it again. Your second point is the sharper one and I have acted on it. The test asserted the prompt's wording, so it locked the error in instead of catching it — wording can always be made to agree with itself. assert script.startswith("cd /workspace &&")
assert "bash -c " in scriptThe wording test stays, but only as a pair with the behavioural one. Two consequences worth flagging, since they reach past this PR. The Super-3.5 evidence run ( |
Second instance of the same mistake in one paragraph: text copied from the published Artificial Analysis prompt without checking it against this backend. Theirs runs as a non-root user in /home/user. GDPValTask builds the provider with working_dir="/root" and prefixes every command with a `cd` to it, so an example rooted at /home/user pointed the model at a directory that does not exist here. The prompt now names /root explicitly and uses it in the example. A test reads working_dir straight out of tasks/gdpval.py and fails if the prompt stops matching it, or if /home/user reappears. Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
…nt runtime (NVIDIA-NeMo#3685) ## What does this PR do? This PR moves the Stirrup sandbox and runtime work from `gdpval-devel` to main. On devel it landed as NVIDIA-NeMo#2338, NVIDIA-NeMo#2395, NVIDIA-NeMo#2588, NVIDIA-NeMo#3086, NVIDIA-NeMo#3121 and NVIDIA-NeMo#3333 (sandbox part). It is one of a series of PRs cut by file from a single tested devel snapshot. It touches `responses_api_agents/stirrup_agent/` and the GDPVal sandbox build script in `nemotron_recipes/`. The GDPVal prompt rewrite that describes this sandbox is in NVIDIA-NeMo#3686, not here. **Sandbox (NVIDIA-NeMo#3333)** - `containers/gdpval.def` now builds the published GDPval-AA v2 environment: - `debian:trixie` with Debian's Python 3.13.5. The build fails on any other interpreter version. - The 419 published Python pins, installed verbatim and checked for exact versions afterwards. - Carlito/Caladea (metric-compatible Calibri/Cambria) and the Noto fonts. - Both published manifests are vendored as plain text: 419 Python pins and the 762-package apt closure. On arm64 the build drops exactly the 5 pins in `gdpval_aa_v2_arm64_exclusions.txt`. Any other pin that fails to install fails the build. - `verify_gdpval_sandbox.py` is staged into the image. It checks a built `.sif` against both manifests, then runs the toolchain end to end. - The definition stages its manifests with relative `%files` paths, so it has to be built from `containers/`. The README command is updated for that. `nemotron_recipes/.../build-gdpval-sif.sh` now builds from the definition's directory, where before it built from the repo root, which fails with this definition. - `_build_command_script` is pulled out of `ApptainerCodeExecToolProvider._exec` with no behaviour change. A test runs two provider-built commands in one bash and checks that only files persist between calls. **Model client and agent (NVIDIA-NeMo#2338, NVIDIA-NeMo#3121)** - `DynamicMaxTokensChatCompletionsClient`: - When the tokenizer renders the complete prompt, the remaining context also caps `max_completion_tokens`. A prompt with no room left raises Stirrup's `ContextOverflowError` before the request is sent. - For token counting only, tool-call arguments are JSON-decoded before template rendering, as vLLM does. - Without a tokenizer, or when tokenization fails, the character estimate counts the whole JSON-serialized history, tool calls included. - New arguments: - `min_completion_tokens`, default 1024 (the old constant). - `prompt_estimator_truncate_history_thinking`, default `None`. It only affects the estimate. - `truncation_recovery`, default `False`. When on, a turn that uses its whole budget without a schema-valid tool call is followed by one turn that requests thinking off and adds a one-time instruction. The instruction is not recorded in the trajectory. - `NeMoAgent.summarize_messages`: - Strips a leading `<think>` block from compaction summaries. - Makes up to 3 summary attempts. Retries add a stricter text-only instruction, and the last attempt runs without tools. If all 3 fail it raises instead of wiping the history. - New `min_compaction_summary_words` argument, default 1. **Provider payload and trajectories (NVIDIA-NeMo#2395, NVIDIA-NeMo#2588, NVIDIA-NeMo#3086)** - `to_provider_openai_messages` sends only the standard OpenAI message keys: - Stirrup's serializer also emits `reasoning_content`, `thinking_blocks` and `metadata` on assistant messages, flat `tool_call_id`/`name`/`arguments`/`signature` fields on tool calls, and `name` on tool messages. - Main's strict chat schema rejects those fields. So any multi-turn request whose history has an assistant turn with tool calls or reasoning currently fails against a Gym model server. - Unparsed `<tool_call>` / `<function=...>` blocks are removed from the provider copy of assistant text. This applies only to assistant turns with no parsed `tool_calls`, and an unclosed block is stripped to the end of the message. The recorded history keeps them. - `convert_stirrup_history_to_output_items` makes `call_id` unique within a response when the server reuses ids across turns (e.g. `code_exec:0`). Call/output pairing is preserved, and trajectories without repeated ids are unchanged. - Tavily `web_fetch`: - If markdown extraction is empty, it tries a second plain-text pass that includes comments. If that is also empty it returns an explicit error. - An extraction under 1% of a page of 20,000+ characters gets a "likely a fragment" note at the start. The new client and agent options are documented in the Stirrup README as constructor arguments. The agent config keys that set them come with NVIDIA-NeMo#3686, which owns `stirrup_agent/app.py`. **Issue:** none. This promotes work that was already reviewed and merged on `gdpval-devel`. **Dependencies:** - CI: none. This PR passes the Stirrup and GDPVal suites on main by itself. - Runtime, already true on main before this PR: main's strict chat schema returns 422 for the `chat_template_kwargs` that Stirrup's client sends on every call. Running Stirrup against a Gym vLLM model server therefore needs NVIDIA-NeMo#3682. - Thinking control: under NVIDIA-NeMo#3682's current default (`forward_request_chat_template_kwargs: false`), the model server accepts request `chat_template_kwargs` and drops them. That means Stirrup's `enable_thinking`, and the thinking-off half of `truncation_recovery`, reach vLLM only when the policy model server sets `forward_request_chat_template_kwargs: true`. The GDPVal config in NVIDIA-NeMo#3686 doesn't set it, so there only the recovery instruction takes effect. The default is still open on NVIDIA-NeMo#3682. - NVIDIA-NeMo#3686 (lands after this one): - It brings the GDPVal prompt rewrite (`code_exec`, no shell state between calls, the 5-minute limit, the AA v2 package summary). It also brings the reference-file path fix that prompt relies on, and the tests that tie the prompt to this sandbox's manifests. - It adds the agent config keys `context_window_tokens`, `min_completion_tokens`, `prompt_estimator_truncate_history_thinking`, `min_compaction_summary_words` and `truncation_recovery`. - Until it lands, main's prompt keeps advertising `run_shell` and packages the image doesn't have (torch, keras, jax, dlib). Main's current image doesn't have them either. The new options keep their defaults, and the context window still comes from the request's `max_output_tokens` (fallback 262144). ## Validation Main `1c8261080` + only this PR's 17 files, macOS, Python 3.13, per-server venvs: - `cd responses_api_agents/stirrup_agent && python -m pytest tests -q`: **224 passed, 2 skipped**. The skips need GNU `timeout` or a real Apptainer image (`GDPVAL_CONTAINER_PATH`). - `cd resources_servers/gdpval && python -m pytest tests -q` (gdpval imports Stirrup): **156 passed**. - With the rest of the series applied: Stirrup 319 passed / 2 skipped, GDPVal 564 passed. The 12 prompt-content tests that moved to NVIDIA-NeMo#3686 fail against main's current prompt, which is why they live there. - `pre-commit run --all-files`: all hooks pass, no files modified. - Schema probe against main's `NeMoGymChatCompletionCreateParamsNonStreaming`: | History | Stirrup's `to_openai_messages` | `to_provider_openai_messages` | |---|---|---| | Assistant tool call + tool result | rejected (44 errors) | validates | | Assistant reasoning, no tool calls | rejected (14 errors) | validates | | Text only | validates | validates | Adding `chat_template_kwargs` is still rejected until NVIDIA-NeMo#3682. - Build script: I ran it with a stub `apptainer` that checks every `%files` source resolves from the build directory. - The new script passes for the default, a relative and an absolute output path. - Main's script fails: `gdpval_aa_v2_python_requirements.txt` is not found from the repo root. - Container: built and verified with `verify_gdpval_sandbox.py` in NVIDIA-NeMo#3333. The image was not rebuilt for this PR, and only comment lines under `containers/` have changed since. | Arch | Interpreter | Pins | apt-closure entries missing | Verifier failures | |---|---|---|---|---| | x86_64 | 3.13.5 | 419/419 | 0 | 0 | | aarch64 | 3.13.5 | 414/419 (the 5 listed exclusions) | 3 | 0 | - Core unit tests: not run. This PR changes nothing under `nemo_gym/`, `scripts/` or `tests/`. ## Rollouts **Live GDPVal smoke (final code):** all seven PRs of the series applied together on main `1c8261080`, run with the devel-only SLURM launcher on a 4-task AA-v2 comparison smoke (one 4-GPU policy node, CPU judging): rollout 4/4 tasks saved and finished (1h04m, 0 failure rows), Office preparation 7/7 converted, comparison judging 4/4 tasks judged with 0 imputed and 0 excluded, 3 wins / 1 loss. Per-task outcomes match an earlier gdpval-devel run on the same checkpoint, tasks and references. No rate limiting occurred, so the judge retry budget was not exercised live. ## Compatibility and benchmark impact - **Sandbox image** (takes effect only after a rebuild): - Existing `.sif` images keep working. - Build from `containers/` (`cd responses_api_agents/stirrup_agent/containers && apptainer build gdpval.sif gdpval.def`) or with the recipes script. A build started from the repo root cannot find the `%files` inputs. - Python packages: - The base moves from `python:3.13-bookworm` to `debian:trixie`, with Python fixed at 3.13.5. - Python packages become exact pins. Every Python package the old definition installed is still in the pin set. - apt: `unar` is dropped (it isn't in the AA v2 closure), and three packages move to their trixie names. - Deliberate differences from AA v2: we keep a compiler toolchain, we add `espeak-ng`, and the per-command limit is 5 minutes (AA uses 10). - **GDPVal results:** runs on a rebuilt image are not comparable with runs on an image built from the old definition. Package versions, fonts and LibreOffice all change. The prompt change in NVIDIA-NeMo#3686 moves scores further. - **Token budgeting (all Stirrup runs):** - With `model_id` set and a template that renders, `max_completion_tokens` can drop below 1024 near the context limit, and a full context raises `ContextOverflowError` before the request is sent. - Without `model_id`, or when tokenization fails, the estimate is larger for every history, most for tool-heavy ones. Budgets shrink only near the context limit, and the 1024 floor still applies. - **Constructor validation:** `max_completion_tokens_cap`, `min_completion_tokens` and `min_compaction_summary_words` below 1 now raise `ValueError`. - **Logging:** a WARNING is logged every time a call uses its whole completion budget without a usable tool call, even when `truncation_recovery` is off. - **Compaction:** empty or think-only summaries now get up to 3 attempts and then fail the rollout. Before, the run continued with an empty summary. - The override uses private Stirrup internals (`_unwind_context_overflow`, `_active_tools`, `_logger`). They are stable under the existing `stirrup>=0.1.12,<0.2` pin, but a Stirrup minor release may need a follow-up. - **Provider requests:** - The non-standard message keys listed above are no longer sent, so earlier-turn reasoning is not replayed. - Unparsed tool-call blocks are removed from the provider copy of assistant text. - **Trajectories:** a repeated tool `call_id` gets a `#n` suffix from its second occurrence onward. - **`web_fetch`:** - A page with no extractable text now returns `success=False` with an error. Before, it returned an empty body. - A fragmentary extraction gets a note at the start. - **Opt-in behaviour:** `truncation_recovery` is off by default. When on, the recovery instruction is appended as a user message, so the provider can see two user messages in a row. - NVIDIA-NeMo#3686 turns it on for GDPVal and also sets `min_completion_tokens: 8192` and `min_compaction_summary_words: 50`. Other Stirrup users are unaffected. - Until NVIDIA-NeMo#3686 lands, those keys in agent YAML have no effect: `StirrupAgentWrapperConfig` accepts extra keys and ignores them. ## Checklist - [ ] I have read the [contributing guidelines](https://github.com/NVIDIA-NeMo/Gym/blob/main/CONTRIBUTING.md). - [x] The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs. - [x] Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable). - [x] Documentation added or updated, or N/A with justification. - [x] Pre-commit checks pass locally (`pre-commit run --all-files`) (so CI lint/format checks pass). - [x] New source files include the required Apache-2.0 SPDX header. - [x] All commits have DCO sign-off (`git commit -s`) (so the DCO check passes). Signed-off-by: Serge Panev <spanev@nvidia.com>
Our GDPval prompt and container had drifted apart and neither matched the
published GDPval-AA v2 environment.
The prompt advertised torch 2.10.0, keras, jax, dlib, mtcnn, pygraphviz,
pdfkit, imgkit and fuzzywuzzy.
gdpval.definstalled none of them, and noneof them are in the v2 dependency set. That listing has been there since the
original Stirrup import (#1090); #1714 dropped torch from the container but
the prompt never got the matching edit. A model that believes it has PyTorch
plans a PyTorch solution and only finds out mid-trajectory.
The interpreter was also wrong: the published apt closure pins
python3.13=3.13.5, but basing on the docker python:3.13 image gave 3.13.15.
What this does
GDPval-AA v2: 419 pinned Python packages and the 762-package apt closure.
debian:trixieand installs Debian's python3.13, asserting themicro version in
%post. The closure is trixie-pinned (+deb13uN, t64names), so the old bookworm base also shipped older libreoffice/ffmpeg.
and checks afterwards that every pin landed at the published version.
summary, and says explicitly that there is no deep learning framework.
--no-install-recommendshad dropped them, which silently reflows everyOffice document written in Calibri or Cambria and renders CJK as tofu.
verify_gdpval_sandbox.py, staged into the image at/opt/gdpval,which audits a built sif against both snapshots and then exercises the
toolchain end to end.
Deliberate divergences
The only runtime divergence from the published prompt is the per-command time
limit: 5 minutes here against their 10, matching
SHELL_TIMEOUT.An earlier revision of this PR also claimed the working directory and
environment variables carry over between calls. That was wrong and is
fixed in 74b8765, thanks to @spanev. Three things in
apptainer_provider.pymean no shell state survives: every call is prefixedwith
cd <working_dir>, the command runs insidebash -c, and that iswrapped in a
( ... )subshell. Files persist; directories, exportedvariables, functions and aliases do not. So the published wording was
accurate for this backend all along, and the prompt now matches it.
The test for that claim originally asserted the prompt's wording, which
locked the error in rather than catching it.
_build_command_scriptis nowextracted from
_execso the test asserts against the script the provideractually builds.
arm64 builds drop five pins that publish x86_64 wheels with no sdist
(aspose-words, cadquery-ocp, cadquery_vtk, nlopt, and cadquery which depends
on the first). The list is closed and the verifier fails if a build drops
anything else.
Verification
Built and verified on SVG (x86_64) and HSG (aarch64). Verifier runs twice per
build, once with the production
apptainer execflags and once with a roomyscratch dir.
Images published to
.../eval/gdpval/sif/python-3.13.gdpval.gym-80e4fc.sifonboth clusters with a
BUILD_INFOfile recording arch and deviations.Stacked on #3319, so the first commit here is that PR's. It drops out once
#3319 merges.