Skip to content

[GDPVal] Fix long-context budgeting and compaction - #3121

Merged
Kh4L merged 2 commits into
gdpval-develfrom
spanev/gdpval-context-budget-compaction
Sep 9, 2026
Merged

Kh4L merged 2 commits into
gdpval-develfrom
spanev/gdpval-context-budget-compaction

Conversation

@Kh4L

@Kh4L Kh4L commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • size completions against the fully rendered prompt when an exact tokenizer is available, while keeping approximate fallback estimates from causing false context-overflow failures
  • add a configurable completion floor and set GDPVal to 8,192 tokens so long tasks do not collapse into truncated 1,024-token tool calls
  • keep model context capacity separate from the Responses API max_output_tokens; request output limits now cap completion output without shrinking the assumed context window
  • mirror Nemotron historical-reasoning truncation in estimator-only fallback mode without mutating the retained trajectory or provider request
  • recognize length-truncated schema-invalid tool calls and apply a one-turn recovery instead of allowing repeated empty code calls
  • sanitize leading inline reasoning from compaction summaries, retry empty or undersized summaries up to three times, and fail closed instead of replacing useful history
  • keep generic Stirrup defaults compatible; GDPVal opts into a 262,144-token context window, an 8,192-token completion floor, and a 50-word compaction minimum

Motivation

A paired AA/Gym rollout analysis found two harness-level long-context failures. Raw-history fallback counting included reasoning that the served Nemotron template omitted, collapsing later completion budgets to 1,024 tokens and producing repeated invalid tool calls. Separately, reasoning-model compaction responses were replayed with their leading think blocks, adding millions of unnecessary prefill tokens and occasionally replacing progress with near-empty summaries.

Validation

At HEAD 470a5e324:

  • Stirrup component suite: 286 passed
  • focused budgeting, compaction, configuration, and output/context boundary coverage: 115 passed
  • vLLM request-template boundary tests: 3 passed
  • full pre-commit run --all-files: passed
  • pinned iter-12500 tokenizer replay: exact template count 321, direct render count 321
  • offline historical replay: the two budget-regression tasks retain a 64K budget across all 250 recorded prefixes; 94 historical compaction outputs sanitize to zero leaked leading think blocks, with the eight undersized summaries rejected
  • explicit boundary regression: an 8K request output cap remains independent from the 262K model context window, including when the exact prompt is already larger than 8K

HSG real-checkpoint canary for the core budgeting/compaction commit 3e496e220:

  • Slurm job 6924145: COMPLETED, exit 0, 4 GPUs
  • checkpoint: /lustre/fs1/portfolios/llmservice/projects/llmservice_modelalignment_ppo/users/abukharin/super35_dualds0821_official_hf_pipeline/hf_full/full_generalist_prod/iter_12500
  • 3/3 selected long-context tasks completed; 230/230 policy calls; zero rollout or transport failures
  • completion budget min/max: 64K/64K; zero 1,024-token or sub-8K requests
  • two natural 70%-threshold compactions were sanitized, replayed, and completed successfully (385- and 567-word retained summaries)
  • fallback live probe reduced the false raw estimate from 260,270 to 270 tokens and dispatched a 64K completion
  • compaction live probe stripped inline reasoning, retained a 209-word summary, and replayed it successfully
  • immutable receipt: /lustre/fs1/portfolios/llmservice/projects/llmservice_nemotron_ultra/users/spanev/gdpval_context_fix_validation_20260904/results/job-6924145/context-fix-canary-receipt.json

HSG full GDPVal task-only validation at HEAD 470a5e324:

  • exact source-of-truth set: 220/220 canonical rows, 0 failure rows, no duplicate or unexpected task IDs
  • exact vLLM access-log accounting: 15,743/15,743 chat-completion requests returned 200
  • 15,640 persisted policy turns plus 103 internal summary requests; the latter reconcile exactly to 99 accepted compactions and four rejected first attempts that were retried successfully
  • all 99 retained compaction summaries had leading reasoning removed and met the 50-word floor; zero terminal compaction failures
  • every persisted policy-call prompt left enough measured context headroom for the configured completion floor (maximum prompt: 193,343 tokens); 6,972 Ray-visible budget samples were all 64K, with zero 1,024-token or sub-8K samples
  • empty code_exec({}) calls fell from 364 in the historical run to 1; leading-think retained summaries fell from 94 to 0; under-50-word summaries fell from 8 to 0
  • 220 finish-marker files, 202 non-null finishes, 18 max-turn/null finishes; 19 tasks reached 250 turns
  • aggregate concurrency 160 on 32 GPUs; collection wall time 2:55:37; independently scheduled shards consumed 61.4 GPU-hours
  • finalizer job 6927667: COMPLETED, exit 0, all eight central audits PASS
  • canonical rollout SHA-256: 2281eae7c1531f93181ab15dc5dc620f8aa8133bf8077cdd449f5fbf5e3ac966

One shard's campaign-local end-audit helper exited after collection because it used str.splitlines() on a valid JSON string containing U+2028. Its 28/28 LF-delimited rows were intact, and the corrected central audit passed them before canonical publication. This was isolated to the validation helper, not the Gym changes.

The historical comparison used the same ordered 220 tasks, equivalent template semantics, 32 GPUs, and aggregate concurrency 160. Its checkpoint was the pure_lm_ab/hf_text wrapper, while the full validation used official_hf_pipeline/hf_full; their language tensors map one-to-one, but the comparison is directional E2E evidence rather than a code-only causal A/B.

The follow-up HEAD commit's output-limit/context-capacity separation is causally covered by the dedicated 8K/262K regression tests; the full run used an outer limit of 262K and provides outcome evidence for that boundary.

Signed-off-by: Serge Panev <spanev@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Signed-off-by: Serge Panev <spanev@nvidia.com>
@Kh4L
Kh4L requested a review from agronskiy September 8, 2026 19:33
@Kh4L
Kh4L marked this pull request as ready for review September 8, 2026 20:01
@Kh4L
Kh4L merged commit b02503c into gdpval-devel Sep 9, 2026
7 checks passed
@Kh4L
Kh4L deleted the spanev/gdpval-context-budget-compaction branch September 9, 2026 07:34
khazic pushed a commit to khazic/Gym-lao that referenced this pull request Oct 1, 2026
…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>
khazic pushed a commit to khazic/Gym-lao that referenced this pull request Oct 1, 2026
…IA-NeMo#3686)

## What does this PR do?

This is NVIDIA-NeMo#3686 of the series that moves `gdpval-devel` to main, cut by
file. It covers GDPVal judging in the resources server, the GDPVal
benchmark config and README, the local-judge overlays, and the Stirrup
agent's GDPVal task code (`app.py`, `file_reader.py`, `prompts/`). Every
file is copied whole from one snapshot: `gdpval-devel` merged with main,
plus NVIDIA-NeMo#2046's review fixes and pre-promotion fixes, including the fixes
listed at the end of this section. The work comes from devel PRs NVIDIA-NeMo#2103,
NVIDIA-NeMo#2331, NVIDIA-NeMo#2338, NVIDIA-NeMo#2588, NVIDIA-NeMo#3086, NVIDIA-NeMo#3121, NVIDIA-NeMo#3131, NVIDIA-NeMo#3184, NVIDIA-NeMo#3335 and NVIDIA-NeMo#3648, and
from NVIDIA-NeMo#2046.

**Judge transport per provider (comparison mode; NVIDIA-NeMo#2588, NVIDIA-NeMo#3131)**
- Each panel member (GPT-5.5, Gemini 3.1 Pro, Claude Opus 4.8) has its
own fixed-model `openai_model` proxy. All three default to the same
endpoint (`JUDGE_BASE_URL`).
- Each member declares how it receives PDFs and rendered Office files
(`media_mode`: `native_pdf`, `images_and_text` or
`native_pdf_overflow_images`), plus provider limits: native-PDF pages,
documents and bytes, image bytes, video count, raster DPI tiers, and a
serialized-request cap.
- A deterministic preflight removes any member that can't carry a
matchup within its limits. If every member is removed for every matchup,
the row fails as terminal `transport_ineligible`.
- These per-member settings apply to comparison mode only. Rubric mode
samples from the same panel but sends every member the server-level
`judge_media_mode` (native PDFs by default), with no per-member limits
or preflight.
- Audio and video capability are separate flags (`handles_audio`,
`handles_video`, `on_missing_av_judge`). Video goes to video-capable
members. Audio that the routed member can't read is dropped with a
warning.
- Attachments are bounded per file, per section and per request (raw and
encoded bytes), with ZIP member limits and text caps. An attachment left
out for size is replaced by a marker the judge sees.
- Legacy `.doc`, `.ppt` and `.xls` files are preconverted. When two
Office files share a stem, each gets its own `<name>.<ext>.pdf` (NVIDIA-NeMo#2331).
A failed OOXML conversion is retried once from a
python-docx/python-pptx/openpyxl re-save.

**Validity of judge results (NVIDIA-NeMo#2588, NVIDIA-NeMo#3648)**
- Rubric: a truncated, scoreless or unparseable reply is flagged
`invalid_judge_response` with a `scoring_error` instead of scoring 0 or
a partial score.
- Structured rubric only (`rubric_scoring_mode: structured`): negative
criterion points no longer count toward `max_possible` (NVIDIA-NeMo#3648).
- Comparison: the verdict is the last `BOXED[...]` token, a reply with
no verdict is dropped instead of counted as a tie, and the prompt ends
with a final-verdict reminder.
- A request that is too large or overflows the context window fails as
permanent. Throttling is retried; rubric retries now also match 502,
"bad gateway" and "too many requests".
- `strict_comparison_trials` (opt-in) rejects a row unless every planned
vote is valid.
- Stirrup turns an invalid judge response into a retryable
`judge_invalid` failure that keeps the finished deliverable
(`reuse_cached_deliverable`), so the retry re-judges it without
re-running the policy.
- Deliverable conversion runs off the event loop.

**Local judges (NVIDIA-NeMo#2046, NVIDIA-NeMo#2103)**
- `judge_media_mode: images_and_text` rasterizes PDF and Office pages
with PyMuPDF and attaches the extracted text (`media_conversion.py`).
`python -m resources_servers.gdpval.inspect_conversion` writes out what
an `images_and_text` judge receives.
- Overlays for a Kimi-K2.6 judge that gym spawns
(`gdpval_kimi_local_judge.yaml`,
`local_vllm_model/configs/moonshotai/Kimi-K2.6.yaml`) and for a
self-hosted MiniMax-M3 endpoint
(`gdpval_minimax_selfhosted_judge.yaml`).

**Inputs (NVIDIA-NeMo#3131, NVIDIA-NeMo#3335)**
- `judge_reference_files_recursive` (opt-in) includes nested files under
a task's `reference_files/`. `judge_reference_files_from_eval` (opt-in)
reads `reference_files/` from the eval task directory instead of each
reference model's directory.
- `count_eval_missing_as_loss` with `missing_eval_task_ids` (opt-in,
multi-stage `stage_index` 1 only) scores a listed task with no eval
deliverable as a loss. The resources server and the Stirrup agent must
both set them.
- `setup_libreoffice.py` skips the reinstall when LibreOffice's
`javaldx` works, and the apt timeout is 1800 s.

**Stirrup GDPVal task code (NVIDIA-NeMo#2338, NVIDIA-NeMo#2588, NVIDIA-NeMo#3121, NVIDIA-NeMo#3131, NVIDIA-NeMo#3184)**
- New agent config fields, defaulting to the old behaviour:
`context_window_tokens` (passed as the client's `max_tokens`),
`min_completion_tokens`, `prompt_estimator_truncate_history_thinking`
and `truncation_recovery` (passed to NVIDIA-NeMo#3685's client), and
`min_compaction_summary_words` (passed to NVIDIA-NeMo#3685's `NeMoAgent`).
- The GDPVal prompts (`prompts/`, moved here from NVIDIA-NeMo#3685 so they land
with the code that builds their reference paths) name the registered
`code_exec` tool instead of `run_shell`, list only packages the AA v2
sandbox installs, and state the `/root` working directory, the 5-minute
command limit and that shell state does not persist. Reference files are
listed relative to the sandbox working directory, without the
`gdpval_ref_files_<id>/` prefix.
- The verify cache is keyed by the row's namespace. The file reader
bounds text extraction, sniffs unknown extensions for text, lists
archive contents, and degrades without LibreOffice.

**Benchmark config**
- Starts the three member proxies alongside `gdpval_judge_model`, each
with `max_http_attempts: 1800`, and sets each member's media limits.
- Claude judge: `thinking: {type: adaptive}` with `output_config.effort:
high` and no temperature.
- `judge_pdf_max_pages: 1000`.
- Stirrup: `min_completion_tokens: 8192`, `context_window_tokens:
262144`, `min_compaction_summary_words: 50`, `truncation_recovery:
true`.
- NVIDIA-NeMo#3684's `retry_terminal_timeouts` and `retry_invalid_judge_responses`,
both `true`.

**Fixes made for this PR**
- The removed `handles_audio_video` member key is now migrated to both
new flags with a deprecation warning; combining it with either new flag
is an error. Before, it was silently ignored.
- `inspect_conversion.py` crashed at start (`ImportError` on
`IGNORE_FILES`). It and `comparison.py` now read `IGNORE_FILES` from
`nemo_gym.deliverables`.
- README and config comments corrected: per-member proxies, Claude's
adaptive thinking, the comparison-only scope of per-member transport,
how the page cap works in comparison mode, the judge timeout and retry
layers, and the Kimi overlay. The README now also documents the judge
input options, attachment environment variables and aggregate metrics.
- New tests for the overlays, the comparison retry loop, truncated
rubric JSON, the inspector, and the Stirrup client and agent arguments.
- The NVIDIA-NeMo#3666 media-preparation helpers (`media.py`, and
`check_reference_inputs` and `office` moved into it from
`preconvert.py`) have no caller in this series and stay on
`gdpval-devel`.

Issue: none. Most of this was reviewed in the devel PRs above; the fixes
in the last group are new here. Related: NVIDIA-NeMo#2046, whose changes and review
fixes are included.

**Depends on NVIDIA-NeMo#3682, NVIDIA-NeMo#3685 and NVIDIA-NeMo#3684; merge after them.** It does not
need NVIDIA-NeMo#3681 or NVIDIA-NeMo#3683.
- NVIDIA-NeMo#3684: `comparison.py` and Stirrup's `file_reader.py` import
`nemo_gym/deliverables.py` at module scope, so both suites fail to
collect without it. The benchmark config sets C3's two retry flags,
which main's config silently ignores. C3's rollout collection also
carries `reuse_cached_deliverable` onto the retried row; without it a
`judge_invalid` retry re-runs the policy.
- NVIDIA-NeMo#3682: the Claude judge sends `thinking` and `output_config`, which
main's strict chat schema rejects with a 422. Main's audio part also
accepts only wav and mp3.
- NVIDIA-NeMo#3685: Stirrup's `app.py` passes the new arguments to NVIDIA-NeMo#3685's client
and agent; main's client raises `TypeError: ... unexpected keyword
argument 'min_completion_tokens'`. The prompt tests read NVIDIA-NeMo#3685's
vendored sandbox manifests.
- Open on NVIDIA-NeMo#3682: under its current default
(`forward_request_chat_template_kwargs: false`), `vllm_model` drops
request `chat_template_kwargs`. The thinking-off half of
`truncation_recovery` then does not reach a policy served by
`vllm_model`; only the one-turn instruction applies. This config does
not set the flag.

## Validation

macOS, Python 3.13. Each server suite ran in a venv built from that
server's `requirements.txt`, with `PYTHONPATH` set to the tree under
test.

```bash
cd resources_servers/gdpval && python -m pytest tests -q
cd responses_api_agents/stirrup_agent && python -m pytest tests -q
python -m pytest tests/unit_tests/test_rollout_collection.py tests/unit_tests/test_openai_utils.py tests/unit_tests/test_deliverables.py -q
```

| Tree | GDPVal | Stirrup |
|---|---|---|
| main `1c8261080` + NVIDIA-NeMo#3681, C2, C3, S and this PR | 414 passed | 321
passed, 2 skipped |
| Same, with `cryptography` not importable | 411 passed, 3 skipped
(`test_media_conversion` text extraction) | — |
| Full series, including NVIDIA-NeMo#3687 | 603 passed | 321 passed, 2 skipped |

The three core unit files: 507 passed on the first tree.

Each dependency was confirmed by removing one PR's files:

| Removed | GDPVal | Stirrup |
|---|---|---|
| NVIDIA-NeMo#3681 | no change | no change |
| NVIDIA-NeMo#3682 | 5 failed: 4 cases of
`test_benchmark_panel_reaches_intended_upstream_models_and_credentials`,
and `test_every_supported_audio_extension_passes_proxy_ingress` | no
change |
| NVIDIA-NeMo#3684 | collection errors (`No module named 'nemo_gym.deliverables'`)
| collection errors, same cause |
| NVIDIA-NeMo#3685 | no change | 5 failed: 4 prompt tests that read NVIDIA-NeMo#3685's
manifests, and `test_runtime_knobs_reach_the_client_and_agent` (the
`TypeError` above) |
| This PR alone | collection errors, same cause | collection errors,
same cause |

`pre-commit run --files` on this PR's files passed with no changes; the
checklist item below is scoped the same way. `uvx ruff@0.9.9 format
--check` and `check` are clean on all 44 files, and new files carry the
SPDX header.

## 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.

This PR stays a draft until NVIDIA-NeMo#3682, NVIDIA-NeMo#3685 and NVIDIA-NeMo#3684 merge.

## Compatibility and benchmark impact

GDPVal scores after this PR are not comparable with main's. Re-baseline
rubric runs and comparison/ELO runs.

**Configuration**
- Panel members: `handles_audio_video` is replaced by `handles_audio`
and `handles_video` (the old key is migrated with a warning). Member
names must resolve uniquely, or verify raises `ValueError`. A
`native_pdf_overflow_images` member needs `max_native_pdf_pages` and
`max_native_pdf_bytes_per_document`.
- New resources-server fields (defaults): `judge_media_mode`
(`native_pdf`), `judge_pdf_render_dpi` (144), `judge_pdf_max_pages` (50;
the benchmark sets 1000), `judge_pdf_include_text` (true),
`judge_max_images_per_request` (450), `judge_handles_audio` /
`judge_handles_video` (false), `on_missing_av_judge` (`warn`),
`judge_reference_files_recursive` / `judge_reference_files_from_eval`
(false), `strict_comparison_trials` (false),
`count_eval_missing_as_loss` (false), `missing_eval_task_ids` (`[]`),
and the per-member transport fields (unset means no limit).
- In comparison mode a raster judge renders up to
`max(judge_pdf_max_pages, judge_max_images_per_request)` pages per file,
and is not used for a matchup whose pages and images exceed
`judge_max_images_per_request`. The overlays' `judge_pdf_max_pages: 30`
therefore bounds rubric mode only.
- New environment variables: `GDPVAL_JUDGE_REQUEST_TIMEOUT_SECONDS`
(comparison default goes from 120 s to 300 s; rubric is 1800 s, where
the SDK's 600 s with two retries applied before),
`GDPVAL_MAX_FILE_BYTES_FOR_JUDGE` (250 MiB, as before, now overridable),
the five section/total attachment and request limits, `JUDGE_GPT_MODEL`,
`JUDGE_GEMINI_MODEL`, `JUDGE_CLAUDE_MODEL`, `JUDGE_GEMINI_API_KEY`
(falls back to `JUDGE_API_KEY`) and
`GDPVAL_GEMINI_MAX_CONCURRENT_REQUESTS` (2).
- Judge SDK clients use `max_retries=0`; GDPVal's own loops and each
proxy's `max_http_attempts` retry instead. `max_http_attempts: 1800`
gives about 15 minutes of retries at 0.5 s per request. Main's retry set
also covers 404, 408 and 500, so a persistent 404 (such as an unknown
model route) takes the whole window to surface. A comparison call times
out after 300 s, is not retried, and time queued behind the Gemini
proxy's 2-request limit counts toward it; the proxy can keep retrying an
abandoned call until its window closes. The README documents how to
lower the window per proxy.
- The Claude proxy's `extra_body` sends `timeout: 900` and
`request_timeout: 900` in every request, as gateway-side keys for the
default endpoint. Endpoints that reject unknown fields need them
removed.
- The benchmark config starts `gdpval_gpt55_judge_model`,
`gdpval_gemini31_judge_model` and `gdpval_claude48_judge_model`
alongside `gdpval_judge_model`. With `retry_terminal_timeouts: true`,
`reference_missing`, `eval_missing` and `transport_ineligible` are
terminal within a run and re-checked on `--resume` (NVIDIA-NeMo#3684).

**Changes that move GDPVal scores**
- Rubric: invalid judge replies no longer score 0 or a partial score.
The resources server's rubric `aggregate_metrics` leaves
`invalid_judge_response` rows out of the mean for any agent, and adds
`rubric/aggregate_rows_total`, `rubric/aggregate_rows_included`,
`rubric/legacy_invalid_rows_excluded` and
`rubric/aggregate_rows_included_fraction`.
- Structured rubric: negative points are excluded from `max_possible`.
Binary mode is unchanged.
- Comparison verdicts: last `BOXED[...]` token, no-verdict replies
dropped, final-verdict reminder in the prompt. `eval_missing` (no
`finish_params.json`) was scored as a loss; it is now a terminal failure
in the sidecar, like `reference_missing` and `transport_ineligible`.
- Comparison payloads: an Office file's rendered PDF is attached once
(main attached it under both names). Office files with no render and
file types the judge can't read are left out instead of sent as filename
stubs. Every rendered `.xlsx` also carries a structured-cells text block
(formulas and cached values, up to 120k characters). Each member sees
PDFs in its own representation and within its own limits.
- Comparison aggregates: when rows carry `expected_final_stage_index`
(written by NVIDIA-NeMo#3687), the headline `comparison/eval_elo` comes from that
stage. If it is missing, incomplete or unfit, the headline is withheld
(`comparison/final_stage_degraded=1`) rather than taken from another
stage; rows without the field keep the last-stage headline. New keys:
`comparison/stage_<k>/{judged_tasks,judged_votes,imputed_loss_tasks,imputed_loss_votes}`,
`comparison/headline_stage_index`, `comparison/final_stage_*` and
`comparison/expected_final_stage_*`. `comparison/win_rate` is omitted
when no vote was judged. `judge_response` gains `total_invalid` and a
per-judge `invalid_count`.
- Claude judge: adaptive thinking with high effort and no temperature
(was `thinking: enabled`).
- Policy (set in the GDPVal config): `min_completion_tokens: 8192` (the
code default stays 1024), `min_compaction_summary_words: 50` and
`truncation_recovery: true`. The rewritten prompt names the real tool,
package set and working directory, and its reference-file paths exist in
the sandbox.

**Every `stirrup_agent` config**
- `timeout_exceeded` is no longer stamped terminal; on `--resume` it is
retried up to `NEMO_GYM_MAX_ROLLOUT_ATTEMPTS`.
- A verify result with `invalid_judge_response` becomes a failure
instead of a scored row: retryable `judge_invalid`, or terminal
`permanent` when the server sets `invalid_judge_retryable: false`.
- A `/verify` error whose text matches GDPVal's permanent markers (413,
"maximum context length", "too many tokens", "input is too long" and
similar) is stamped terminal `permanent` and not retried, unless a
throttle marker is also present.
- The request's `max_output_tokens` now caps completion tokens; the
context window comes from `context_window_tokens` (default 262144).
- Failure rows after `/verify` carry the policy response plus
`deliverables_dir`, `elapsed_seconds` and `_ng_timeout_inflight`.
`judge_only` and `rerun_incomplete` fall back to a flat `task_<id>/`
directory.
- Stirrup now imports `resources_servers.gdpval` (`scoring`,
`judge_panel`, `preconvert`, `media_conversion`), while the GDPVal
server already imports Stirrup's file reader.

**Known limitations**
- `requirements.txt` lists `cryptography>=42.0`, but the root
`pyproject.toml` `exclude-dependencies` drops it from server venvs, and
an `overrides.txt` does not bring it back. Without it, pdfminer can't
load: the `images_and_text` text copy is empty (a warning is logged), 3
`test_media_conversion` tests skip, and rubric `read_deliverable_files`
returns an error string for PDFs (also true on main). The fix is a
change to the root `pyproject.toml`.
- Rubric judge sampling is seeded per task, so a `judge_invalid` retry
draws the same member. A provider that rejects a task's payload in
rubric mode fails every attempt until the attempt limit.
- ZIP members skipped by the archive limits are logged but not marked in
the judge request.
- The README's multi-stage sections describe `partial_completion` and
other behaviour that lands with NVIDIA-NeMo#3687.

## Checklist

- [x] 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 (scoped: `pre-commit run --files`
on this PR's files).
- [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>
Co-authored-by: Marta Stepniewska-Dziubinska <marta-sd@users.noreply.github.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.

2 participants