Skip to content

fix(gdpval): run Pi sandbox sessions end to end and score them with multistage ELO - #2

Draft
athitten wants to merge 23 commits into
codex/pi-gdpval-sandbox-prototypefrom
pi-gdpval-review-fixes
Draft

athitten wants to merge 23 commits into
codex/pi-gdpval-sandbox-prototypefrom
pi-gdpval-review-fixes

Conversation

@athitten

@athitten athitten commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Stacked on yaoyu-33#8 (Pi in GDPval task sandboxes), which is stacked on NVIDIA-NeMo#3626. This PR fixes what
blocked a full 220-task Pi × GDPval run on HSG. It also adds a way to score sandbox-session runs with the GDPval-AA v2
multistage ELO: gym eval run --no-serve cannot drive multistage.

Fixes, grouped by problem:

  • Seeding: a seed retry reused a stopped sandbox, and reference lists stored as JSON strings broke staging
    (29edfc233).
  • Large references: two tasks have reference files over the 128 MiB limit (up to 689 MB), so they could never
    start. There is now a 1 GiB per-file cap with bounded download retries. A missing file fails the task with a terminal
    424 and an oversized one with a terminal 413, instead of an endlessly retried 500. 408, 425, 429, 5xx and malformed
    responses are retried (ef48c21ee, 6fe5f612a, 65274f632, 802476d34).
  • Export layout: a subdirectory, link, unusual name, or more than 100 files in /workspace/output failed the
    episode as an infrastructure error, which dropped the task from the score. The export now copies the top-level
    regular files (all the judge reads). Everything else is recorded in finish_params.json under skipped: the first
    100 entries plus a count. A missing or symlinked output directory is recorded too, and the in-sandbox listing is
    capped at 10,000 entries. The export cap is now 1 GiB, because two gold deliverables exceed 128 MiB (abdec3d38,
    925b786fe, e18f1f5bc, 12f0d65b1, 75aaa5eeb).
  • Completion marker: the export writes the finish_params.json marker, so comparison mode judges the export
    instead of reporting it missing (6020761bf).
  • Judge failures no longer lose the export:
    • An invalid verdict used to raise a 503. That re-ran the whole episode with the same seeded judge, and the export
      never reached the ELO.
    • A raised judge error, such as a 413 or context-window error, did the same. That happened once in the run below
      (Claude on Bedrock, "Input is too long").
    • Both now return a masked verdict (mask_sample, failure_kind, failure_reason) that keeps deliverables_dir.
      The rubric mean and gym eval profile exclude it, and judge-only scoring re-judges the export (f54d81898,
      7290f2abb, 22f6dede0).
  • LibreOffice: clusters without a host LibreOffice can set libreoffice_command, for example to run it through
    the GDP image with apptainer exec. The startup probe checks that the command can see deliverables_root as well as
    TMPDIR (24e59f9c1, 93e6cbb6a).

Adds:

  • resources_servers/gdpval/cache_sandbox_deliverables.py copies sandbox exports into the task_<id>/repeat_<n>
    layout that judge-only scoring reads. Multistage ELO then runs as a second, judge-only pass. The cache build stages
    copies outside the task directories, and it fails, naming the tasks, when an export has no marker (eee1fd325,
    4183b4b2a, 9fccc683c, 57ff3ca01).
  • Docs: the export rules, judge-only scoring of sandbox runs, and libreoffice_command (42c0f2ddd, 8c0c43579).
    They also give the config-order rule for overriding the judge panel: pass --config benchmarks/gdpval/config.yaml
    before your overlay instead of using --benchmark gdpval. The CLI appends --benchmark after every --config, so
    the benchmark's judge settings silently replace yours. The headers of the Kimi and MiniMax judge overlays showed the
    wrong order and are fixed.

Related:

Validation

  • NEMO_GYM_EXTRA_ROOTS=$PWD resources_servers/gdpval/.venv/bin/python -m pytest resources_servers/gdpval/tests -q:
    655 passed, 3 skipped, 4 failed.
    • The 4 failures are in test_judge_panel.py and are the same at a35bc005e: the openai SDK rejects a Starlette
      TestClient as its http_client.
    • The suite also passes at each commit.
  • Each new test was checked by breaking the code it guards. Examples:
    • export skip instead of 503, count and size limits, listing shape and cap, symlinked output root;
    • invalid and raised judge errors masked, checked through the real select_measured; cancellation still propagates;
    • download statuses and fail-closed guard, cache staging and the marker check, the LibreOffice probe directories and
      %PDF check.
  • pre-commit run --files <changed files>: all hooks pass with no changes. ruff check and ruff format --check are
    clean.
  • Two independent review rounds. Their findings are addressed in the last 16 commits; what remains is listed under
    follow-ups.

Rollouts

220-task Pi × GDPval on HSG:

  • Policy: N35 Super GA served by vLLM.
  • Harness: Pi 0.80.2 with MB-1712 budgets (thinking high, 32,768 output tokens per call, 3,600 s per episode),
    concurrency 16.
  • Code: generation ran at f54d81898, across two chained 2-hour jobs with one automatic resume. The later commits
    are covered by unit tests.
  • Scoring: judge-only, with the official AA-v2 panel (GPT-5.5 / Gemini 3.1 Pro / Opus 4.8) and the 18-reference
    ladder.
Metric Value
Tasks 220. There are 219 rows; 1 export was recovered after a raised judge error, the case 22f6dede0 fixes.
Scoring settings 900 s comparison judge timeout. partial_completion floors: 0.9 / 0.3 / 1 for stage 1 and 0.95 / 0.5 / 1 for the final stage. Availability-only reference repair. The Codex run (#1) used the same settings.
Rubric (re-judge from the cache, official panel) 79.3 / 100, with 218 of 220 judged. Two tasks hit permanent judge size limits.
Multistage ELO (comparison/eval_elo) 1150.6. The final stage judged 216 of 220 tasks against deepseek_v4_pro, glm51_fp8, inkling_small and kimi_k26, and was accepted with partial_completion at 98.2% coverage; it is not degraded and has no imputed losses. Stage 1 (45 tasks × 18 references) gave 1194.8. The 4 omitted tasks are all transport_ineligible: their deliverables are larger than every judge's attachment limit.
Episodes that ended on a length-truncated model call 11 (10 scored 0). Pi has no truncation recovery; see follow-ups.

Compatibility and benchmark impact

  • The stateless (Stirrup) GDPval path is unchanged. The default LibreOffice argv is unchanged, and
    libreoffice_command defaults to unset.
  • finish_params.json from sandbox exports gains skipped and skipped_count. Its readers only check that the file
    exists.
  • In sandbox sessions, a judging failure returns a masked row instead of failing /verify with a 500 or 503. This
    covers invalid verdicts, raised judge errors, and inline comparison failures. Score such runs with the judge-only
    pass from the cache.
  • Benchmark impact: none for existing runs. The ELO above is comparable to the 18 anchors: same judge panel and
    multistage protocol.
  • Follow-ups, not in this PR:
    • Pi has no equivalent of Stirrup's truncation_recovery. A call that spends the whole output budget on reasoning
      ends the episode.
    • The Apptainer provider copies files synchronously, so a 1 GiB file blocks the resources-server event loop while it
      copies.
    • The CLI's config order (--benchmark after --config) has no test.

Checklist

  • I have read the contributing guidelines.
  • The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs.
  • Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable).
  • Documentation added or updated, or N/A with justification.
  • Pre-commit checks pass locally (on the changed files).
  • New source files include the required Apache-2.0 SPDX header.
  • All commits have DCO sign-off (git commit -s) (so the DCO check passes).

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Two GDPVal tasks have 220-660 MiB reference files, above the 128 MiB cap, so their seeds always failed and were retried as transient errors. Use a separate 1 GiB per-file cap and an idle-read timeout, bound connection retries, retry 429/5xx a few times, and return a terminal 413 for oversize files.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
A subdirectory, link, unsafe name, or more than 100 files in /workspace/output made the export return 503, which counts as a retryable infra failure, so the task was dropped from the score and re-run on resume. Judges read only top-level regular files, so export those, record skipped entries in finish_params.json, and raise the export cap to 1 GiB (two gold deliverables exceed 128 MiB).

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Raising 503 on an invalid judge verdict sent the row to the failure sidecar without its export, so judge-only ELO could not score it and a resume re-ran the agent while the seeded judge choice repeated. Return the verdict: rubric aggregation already excludes invalid rows and reports them.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
A missing reference or exhausted retries surfaced as a bare aiohttp error, which the middleware turns into a generic 500, so EnvironmentServer re-ran the episode. Return 424 for a non-retryable HTTP status and 503 after the last attempt, and let the download loop own retries (one try per request() call), so the worst case is four bounded attempts.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Sandbox sessions return an invalid judge verdict with a placeholder 0.0 so its export stays available for re-judging. The GDPVal rubric mean skips it, but gym eval profile and trainers only skip rows with mask_sample, so they counted it as a real zero. Set mask_sample on these verdicts.

Also set failure_reason and failure_kind (judge_unparseable for an unscorable reply, judge_failed when there is no judge result, verifier_error for a missing rubric), which Gym expects on masked rows. This is safe: rollout collection sends a row to the failures sidecar only when it carries _ng_failure_class, and resume treats every rollouts.jsonl row as done; neither reads failure_kind or mask_sample. So the row stays in rollouts.jsonl with its deliverables_dir and a resume does not re-run the agent.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Every attempt either returns after a complete write or raises, but nothing enforced that after the loop. A fall-through would let seeding stage whatever bytes sit at the shared scratch path: a partial body or the previous reference. Raise a retryable 503 instead, and test that a body cut off on every attempt is never staged.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The 1 GiB export cap exists because the gold answer for task 75401f7c is a video of about 278 MiB. Assert the cap still admits it, as the reference cap test does for the largest reference file.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The export listing printed [] when /workspace/output was missing or a symlink, so the marker read as "the model saved nothing". The listing now reports whether the directory was usable, and the export records it in finish_params.json "skipped". A listing that is malformed, or lists entries for an unusable directory, is still a 503.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The model decides how many output entries are skipped, so finish_params.json and the warning could grow without limit; transport routing already ignores markers over 16 MiB. Keep the first 100 skipped entries and record the total in skipped_count.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
A failed copy left task_<id>/repeat_<n>.partial, which starts with repeat_, so judge-only scoring listed it as an attempt and could judge a partial export. Stage copies in <output>/.staging/ and remove it at the end.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
A row whose deliverables_dir had no finish_params.json was counted as a plain skip, so a moved, deleted or unmounted export silently became a missing task in judge-only ELO. Report those rows as export_without_marker and exit non-zero listing their task ids; rows with no deliverables_dir stay a plain skip. The summary now lists task ids per outcome.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Document libreoffice_command, the sandbox export rules, and how to score sandbox-session exports with judge-only multi-stage ELO (cache_sandbox_deliverables, readiness timeout, -i, dataset_path). Note that a judge overlay must follow --config benchmarks/gdpval/config.yaml, because --benchmark is appended after every --config, and fix the Kimi example that used the wrong order. Replace stale Stirrup-only and rubric-only wording.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The rubric scorers re-raise permanent judge errors (413, context window), and any bug in grading raises too. /verify then failed, so the row reached the failures sidecar without deliverables_dir, the cache adapter never saw the export, and judge-only ELO scored the task as missing. Catch errors from grading only (export failures still fail /verify) and return the same masked placeholder as an invalid verdict: judge_failed for judge errors, verifier_error otherwise, retryable unless the judge error is permanent. Cancellation still propagates.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Removing either check left the suite green. Test that a libreoffice_command whose probe output is not a PDF fails the startup check, and that a file whose size changed between listing and copy fails the export without writing a marker.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The startup probe converted only in the temp directory, so a container command without a bind for deliverables_root passed and then failed on every export, falling back to text. Also probe in a temp directory under deliverables_root when it is set. Each probe is now one in-place LibreOffice run, like the first attempt of a real conversion: convert_to_pdf retries an xlsx from a temp-directory copy, which would hide the missing bind.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Every status below 500 except 408 and 429 was terminal, including 425 (which the Environment Server retries) and the status 0 that aiohttp reports for some malformed responses. Make only 4xx terminal, excluding 408, 425 and 429.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
An export made before the server wrote finish_params.json has no marker, so the cache build fails on it. Say so in the error, next to the moved or unmounted export case.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The model decides how many entries /workspace/output holds, and the listing printed all of them for the server to parse. Print at most 10,000 (sorted) and report the rest as unlisted; the export counts them in skipped_count, since it keeps at most 100 files anyway.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
The Kimi and MiniMax multi-stage overlay headers ran --benchmark gdpval --config <overlay>. The CLI appends --benchmark after every --config, so the benchmark config won: gym env resolve shows the frontier panel replacing both overlays' judge, and the MiniMax overlay falling back to rubric mode. Load the benchmark with --config before the overlay instead. In the server README, say that a missing output directory is still judged as an empty submission, and cover raised judging errors and the listing limit.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant