Skip to content

[skill] evaluation: add GDPVal (NeMo Gym Stirrup agent) support - #2039

Merged
cjluo-nv merged 16 commits into
mainfrom
chenjiel/gdpval-eval-skill
Aug 3, 2026
Merged

[skill] evaluation: add GDPVal (NeMo Gym Stirrup agent) support#2039
cjluo-nv merged 16 commits into
mainfrom
chenjiel/gdpval-eval-skill

Conversation

@cjluo-nv

@cjluo-nv cjluo-nv commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: new feature (agent skill)

Adds GDPVal support to the evaluation agent skill. GDPVal is an agentic AA
benchmark: the NeMo Gym "Stirrup" agent produces office/PDF deliverables inside a
per-task Apptainer code-exec sandbox, and a judge panel scores them. It runs on the
0.2.6 launcher as a nemo_gym task, but it is standalone (one gym eval per
config) and mechanically unlike the aa/ nemo-skills tasks, so it gets its own
branch in the skill rather than being merged into the aa/ task list.

  • recipes/tasks/aa_gym/gdpval.md — task recipe: standalone rule, rubric-vs-comparison
    scoring, canary, score extraction.
  • references/gym-gdpval.md — the machinery: Apptainer SIF sandbox, the
    _gym_prepare venv-repair / process-group-reap workaround, deployment sizing,
    scoring modes, the MLflow deliverables trap, canary failure modes, and the
    SIF ↔ Gym-version rebuild coupling.
  • recipes/examples/gym_gdpval/ — self-contained SLURM + vLLM template plus the
    co-located _gym_prepare.yaml Hydra include (it must travel with the config).
  • scripts/gdpval-sif.sh — build-if-absent / reuse-if-present Apptainer SIF helper.
    Builds on the target cluster only (never copies across clusters), flock-guarded and
    atomic, driven by $GDPVAL_SIF_DIR.
  • SKILL.md / references/quantization-benchmarks.md — GDPVal is part of the AA
    suite but a different harness, so it is generated as a companion standalone config.
  • recipes/env.exampleTAVILY_API_KEY (agent web search) and GDPVAL_SIF_DIR.

Usage

# 1. Set GDPVAL_SIF_DIR in .env, then build the sandbox once on the target cluster
#    (build-if-absent, reuse-if-present):
srun -p cpu -t 01:00:00 --pty .agents/scripts/gdpval-sif.sh

# 2. Copy the whole example dir (the _gym_prepare.yaml include must travel with it),
#    fill in the ??? values, then dry-run -> canary -> full:
nel run --config gym_gdpval/example_gym_gdpval.yaml --dry-run

Testing

Validated end-to-end on an aarch64 GB300 SLURM cluster with an NVFP4 MoE checkpoint:
deploy → SIF build + sandboxed exec → gym head server → 220 rollouts + deliverables →
judge scoring, producing a real rubric score with zero judge failures. Several traps
found during that run are now documented in the reference (silent unsandboxed
fallback, gym-commit/head-server hang, judge api-key value-vs-name, SIF ↔ Gym version
coupling, limit_samples not limiting gym rollouts).

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: N/A (agent skill documentation + helper script; no library code)
  • Did you update Changelog?: N/A (agent skill only, no API change)
  • Did you get Claude approval on this PR?: ❌ (not yet run)

Additional Information

Docs/skill-only change under .agents/; no modelopt/ source is touched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added standalone GDPVal evaluation support through NeMo Gym, with Slurm, vLLM, sandbox, judge, reasoning, sampling, and logging configuration.
    • Added automated Apptainer/Singularity image setup with reuse, validation, locking, and reliable publishing.
    • Added configurable settings for judge services, web search, and shared image caching.
    • Added GDPVal preparation and execution examples, including dry-run, canary, and full-run guidance.
  • Documentation

    • Added GDPVal setup, troubleshooting, scoring, deployment, and quantization guidance.
    • Clarified separate GDPVal configuration, mandatory thinking mode, and repeat-count requirements.

GDPVal is an agentic AA benchmark: the Stirrup agent produces office/PDF
deliverables in a per-task Apptainer code-exec sandbox, then a judge panel
scores them. It runs on the 0.2.6 launcher as a `nemo_gym` task, but is
standalone (one gym eval per config) and mechanically unlike the `aa/`
nemo-skills tasks, so it gets its own branch in the skill.

- recipes/tasks/aa_gym/gdpval.md: task recipe (standalone rule, scoring
  modes, canary, score extraction).
- references/gym-gdpval.md: SIF sandbox, `_gym_prepare` machinery, deploy
  sizing, rubric-vs-comparison scoring, MLflow deliverables trap, failure
  modes, and the SIF<->Gym-version rebuild coupling.
- recipes/examples/gym_gdpval/: self-contained SLURM + vLLM template plus
  the co-located `_gym_prepare.yaml` Hydra include.
- scripts/gdpval-sif.sh: build-if-absent / reuse-if-present Apptainer SIF
  helper. Builds on the target cluster only (never copies across clusters),
  flock-guarded and atomic, driven by $GDPVAL_SIF_DIR.
- SKILL.md / quantization-benchmarks.md: GDPVal is part of the AA suite but
  a different harness, so it is generated as a companion standalone config
  and never merged into the `aa/` task list.
- env.example: TAVILY_API_KEY (agent web search) and GDPVAL_SIF_DIR.

Validated end-to-end on an aarch64 GB300 cluster: deploy -> SIF build+exec
-> gym head server -> 220 rollouts + deliverables -> judge scoring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@cjluo-nv
cjluo-nv requested a review from a team as a code owner July 31, 2026 21:43
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a GDPVal SIF builder and a standalone NeMo Gym evaluation path. The change includes runtime preparation, sandbox and judge configuration, environment examples, staged execution guidance, score extraction, and separate GDPVal handling in AA quantization recommendations.

Changes

GDPVal evaluation

Layer / File(s) Summary
GDPVal SIF builder
.agents/scripts/gdpval-sif.sh
Adds path resolution, image reuse, locked builds, runtime discovery, fakeroot fallback, validation, cleanup, and atomic publication.
Standalone GDPVal evaluation configuration
.agents/skills/evaluation/recipes/env.example, .agents/skills/evaluation/recipes/examples/gym_gdpval/example_gym_gdpval.yaml
Adds environment settings, Slurm and vLLM deployment, judge integration, rollout controls, sandbox setup, and MLflow export.
Gym runtime preparation
.agents/skills/evaluation/recipes/examples/gym_gdpval/_gym_prepare.yaml
Adds Gym commit selection, dependency repair, environment setup, rollout execution, process cleanup, and Ray shutdown.
GDPVal workflow and benchmark guidance
.agents/skills/evaluation/SKILL.md, .agents/skills/evaluation/recipes/tasks/aa_gym/gdpval.md, .agents/skills/evaluation/references/gym-gdpval.md, .agents/skills/evaluation/references/quantization-benchmarks.md
Documents standalone execution, SIF and judge requirements, staged validation, score extraction, and separate GDPVal inclusion in AA recommendations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant nel
  participant vLLM
  participant nemo_gym
  participant GDPValJudge
  nel->>vLLM: Deploy reasoning-enabled policy
  nel->>nemo_gym: Start pinned GDPVal task
  nemo_gym->>vLLM: Collect policy rollouts
  nemo_gym->>GDPValJudge: Submit outputs for scoring
  GDPValJudge-->>nemo_gym: Return scores and deliverables
  nemo_gym-->>nel: Export results to MLflow
Loading
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the addition of GDPVal support to the evaluation skill.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns ✅ Passed The full PR adds no Python or dependency-manifest files. Added-line scans found none of the prohibited loaders, trust_remote_code=True, eval/exec, or # nosec patterns.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chenjiel/gdpval-eval-skill

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.24%. Comparing base (01c708e) to head (3f8c7dd).
⚠️ Report is 26 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2039      +/-   ##
==========================================
- Coverage   75.81%   74.24%   -1.58%     
==========================================
  Files         518      520       +2     
  Lines       58574    62737    +4163     
==========================================
+ Hits        44406    46577    +2171     
- Misses      14168    16160    +1992     
Flag Coverage Δ
unit 55.25% <ø> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/scripts/gdpval-sif.sh:
- Around line 45-46: Update GDPVAL_GYM_COMMIT and GDPVAL_SIF_NAME defaults in
.agents/scripts/gdpval-sif.sh to dd41196f620f2af99947d776cbe5da9439d2a08d and
python-3.13.gdpval.sif. In
.agents/skills/evaluation/recipes/examples/gym_gdpval/example_gym_gdpval.yaml:30-40,
no direct change is needed once the script defaults are corrected. Update the
documented SIF path to python-3.13.gdpval.sif in
.agents/skills/evaluation/SKILL.md:64-70,
.agents/skills/evaluation/recipes/tasks/aa_gym/gdpval.md:23-28, and
.agents/skills/evaluation/references/gym-gdpval.md:22-29.
- Around line 108-123: Update both Apptainer build invocations in the fakeroot
fallback flow to include the --force option, ensuring the commands using
"$APPTAINER_BIN" overwrite an existing "$tmp" without prompting.

In @.agents/skills/evaluation/recipes/examples/gym_gdpval/_gym_prepare.yaml:
- Around line 27-70: Update the final PYTHONPATH export in gym_prepare to
preserve any inherited $PYTHONPATH while prepending /opt/Gym and the main venv
site-packages. Use a shell conditional or equivalent that references $PYTHONPATH
without ${...} syntax, and keep the existing path ordering for Gym and its
dependencies.

In
@.agents/skills/evaluation/recipes/examples/gym_gdpval/example_gym_gdpval.yaml:
- Around line 166-175: Update the pre_cmd installation flow to detect the
container architecture and install an architecture-compatible Apptainer runtime,
avoiding the hardcoded amd64 1.4.2 package on aarch64 where no matching release
exists. Ensure installation failure is not masked by the current &&/fallback
behavior: verify Apptainer is available after installation and explicitly
terminate pre_cmd if it is unavailable, preserving the existing squashfuse setup
only after Apptainer succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 963909ee-d867-411c-a876-056e093a3662

📥 Commits

Reviewing files that changed from the base of the PR and between a23390d and 0c6a105.

📒 Files selected for processing (8)
  • .agents/scripts/gdpval-sif.sh
  • .agents/skills/evaluation/SKILL.md
  • .agents/skills/evaluation/recipes/env.example
  • .agents/skills/evaluation/recipes/examples/gym_gdpval/_gym_prepare.yaml
  • .agents/skills/evaluation/recipes/examples/gym_gdpval/example_gym_gdpval.yaml
  • .agents/skills/evaluation/recipes/tasks/aa_gym/gdpval.md
  • .agents/skills/evaluation/references/gym-gdpval.md
  • .agents/skills/evaluation/references/quantization-benchmarks.md

Comment thread .agents/scripts/gdpval-sif.sh Outdated
Comment thread .agents/scripts/gdpval-sif.sh
Comment thread .agents/skills/evaluation/recipes/examples/gym_gdpval/_gym_prepare.yaml Outdated
Corrections found while bringing GDPVal up on an aarch64 GB300 cluster, in
both rubric and comparison mode.

Correctness:
- _gym_prepare: run the rollout command from a script written via a QUOTED
  heredoc instead of `bash -c '...'`. Gym params legitimately contain single
  quotes (comparison mode's ++multistage.stages='[{num_tasks: 45}, ...]' and
  ++...judge_panel='[{...}]'), which closed the wrapper early so Hydra got the
  value split on spaces and died with "no viable alternative at input
  '[{num_tasks:'". The quoted delimiter keeps $$ and $*_API_KEY unexpanded
  until run time.
- example: never put '#' comments inside a folded (>-) params scalar. YAML
  keeps them as literal text; the block folds to one line and the first '#'
  comments out every override after it in the shell command.
- example: arch-aware pre_cmd (Ubuntu PPA) — the hardcoded amd64 apptainer
  .deb cannot install on aarch64, which is most Blackwell/Grace clusters.
- gdpval-sif.sh: align the default SIF name and gym commit with the template,
  so build and run no longer disagree by default.

Scoping (rubric default; comparison is a conversion):
- The template is now rubric-only and self-consistent: no dangling
  reference_elo / reference_deliverables_dir, which CONFLICT with comparison
  mode's reference_models map. Comparison needs a reference set, a newer gym
  image, and its own overrides -- documented, not half-wired here.

Documented traps:
- install_on_the_fly.commit is INERT on images that bake Gym as a non-git
  directory (the public nemo-gym image): the prepare step logs "/opt/Gym is
  not a git repo" and the pin is ignored. Do not attribute behaviour changes
  to it without "=== NeMo Gym commit ===" + a SHA in the log; a port-11000
  head-server hang is a transient collision, not a version symptom.
- What NEL validates and what it does not: mount dirs fail loudly before
  sbatch, but `test -d` cannot see a missing/misnamed SIF *file* (silent
  unsandboxed fallback) and the container is never checked; --dry-run skips
  remote validation entirely. Adds `gdpval-sif.sh --check` as the preflight.
- num_repeats depends on the flow: multistage comparison uses 1 (top-level
  ++num_repeats works), pre-multistage rubric/single-ref used 2.
- Prefer a site-provided SIF over building, when one exists: a self-built SIF
  resolves its pip stack at build time and can drift from the sandbox a
  published reference set was generated in.

Validated on aws-cmh (GB300, aarch64): deploy -> SIF build+sandboxed exec ->
gym head server -> rollouts + deliverables -> judge scoring, in rubric mode
(219/220 tasks, 0 judge failures) and in comparison mode against a 9-reference
set (verify_mode=comparison, per-reference win/loss/tie).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
@.agents/skills/evaluation/recipes/examples/gym_gdpval/example_gym_gdpval.yaml:
- Around line 175-183: Update the pre_cmd dependency-installation flow to remove
“|| true” from the apt-get install command so missing squashfuse or fuse3 causes
the setup to fail. Keep apptainer installation validation and add an SIF
execution canary on the target cluster to verify mount dependencies are usable,
not merely that the binary exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3adfdf25-b2a6-4f9a-bc34-7f661848dc9f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c6a105 and 74dfa8b.

📒 Files selected for processing (5)
  • .agents/scripts/gdpval-sif.sh
  • .agents/skills/evaluation/recipes/examples/gym_gdpval/_gym_prepare.yaml
  • .agents/skills/evaluation/recipes/examples/gym_gdpval/example_gym_gdpval.yaml
  • .agents/skills/evaluation/recipes/tasks/aa_gym/gdpval.md
  • .agents/skills/evaluation/references/gym-gdpval.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .agents/skills/evaluation/recipes/tasks/aa_gym/gdpval.md

cjluo-nv and others added 6 commits August 2, 2026 23:36
Reading the final score off a finished GDPVal run was guesswork: the recipe
just said the metric "is logged under nemo_gym.gdpval in MLflow".

Two traps this fixes, both hit on a real run:
- `artifacts/eval_factory_metrics.json` does NOT contain the GDPVal score. It
  holds only response_stats / reasoning / evaluation (request telemetry), so
  looking there and finding no ELO reads like the run failed to score.
- A comparison run logs ~200 MLflow metrics of which ~126 are per-reference
  (/ref/<model>/...), so the three headline numbers are easy to miss in the UI.

Documents the authoritative local file (artifacts/results.yml) with the exact
metric paths for both modes, the MLflow key names (including the key_metrics/
duplicates), a copy-pasteable one-liner that reads the ELO without MLflow, and
sanity checks (judged count, num_stages/num_references, unique task_id count vs
220) so a score computed on a short task set is not quoted as final.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
normalized_elo is the AA 0-1 scale and the number that is comparable across
models and to the published AA index; eval_elo is the same Bradley-Terry fit
on the raw Elo axis (normalized_elo = (eval_elo - 500) / 2000). Make the
recipe say which one to report instead of leaving the reader to choose:
normalized_elo leads the metric table, the extraction one-liner, and the
MLflow key list, with eval_elo demoted to supporting detail.

Also fixes three places where the recipe contradicted itself after the
rubric/comparison split:
- the header asserted num_repeats=2 "in the reviewed golden" while the Config
  section (correctly) makes it flow-dependent;
- the SIF filename still said python-3.12 while the template ships 3.13;
- "Scoring modes" still described the old single-reference two-step flow
  (mount at /gdpval/refs/test_ref, set reference_elo=1290), which no longer
  matches the multi-reference design and would send a reader down a path that
  fails at startup. It now states that comparison is a conversion, not a flag
  flip, and points at the conversion checklist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
…gen test

Regenerating a config from the skills alone (agent blocked from seeing the
known-good answer) reproduced every score-determining setting exactly, but
surfaced contradictions the shared reference had been left holding.

- The SIF path said `/gdpval/sif/python-3.12.gdpval.sif` while SKILL.md, the
  task recipe and the template all say 3.13. This is the one string the file
  itself stresses must match in two places, and a mismatch degrades silently to
  non-sandboxed exec -- the worst possible thing to be stale.
- The num_repeats section still ended with "remove the sed line to keep 2 for
  golden-comparable / reported scores", which is wrong for the multistage
  comparison flow (1) that the recipe and eval-config both specify. Since
  SKILL.md sends readers to this reference first, it was the answer they would
  find. Now split by flow.

Also documents three things a generating agent had to guess:
- `--max-num-seqs` must come from `stirrup_agent.concurrency`, not from
  `parallelism` (which is gym-internal); the generic rule yields an absurd cap.
- `max_new_tokens` legitimately does not apply on the gym path (the adapter
  puts it in params_to_remove), so the six-field template is not violated.
- temperature/top_p should match whatever the reference deliverables were
  generated with, since a sampling difference shows up in a pairwise ELO as if
  it were a quality difference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Checked the designated golden (Qwen3.6-35B-A3B): it uses num_repeats=1, set as
a plain top-level `++num_repeats=1` in the multistage block, with no `sed`
patching anywhere. The `=2` in the skill came from the older MiniMax-M2.7
single-reference golden, which predates multistage.

So standardise on 1 instead of making the reader work out which flow they are
in:
- template: drop the `sed 's/num_repeats: 2$/num_repeats: 1/'` workaround and
  its "the reviewed GOLDEN uses 2 / delete this line for reported scores"
  comment (both wrong now), and set `++num_repeats=1` the way the golden does.
  The pinned Gym already ships 1, so the override is belt-and-braces.
- recipe + reference: collapse the two-branch explanation to "use 1", with the
  pre-multistage 2 kept only as a one-line historical note so nobody carries it
  forward from an old config.

This also removes the last claim that num_repeats cannot be set via `++` -- it
can, on current pins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
The judge details were living in the internal eval-config skill, but nothing
about them is internal: the panel composition, the failure modes and the
throttle values are properties of GDPVal itself, useful to anyone running it.
Only the concrete endpoint URL is site-specific, and the public skill already
has a convention for that (`<INFERENCE_JUDGE_URL>` from `.env`).

Adds a Judge section to the reference covering:
- rubric uses one judge; comparison uses a 3-member panel sampled per trial,
  with judge_sampling_seed making the sampling reproducible;
- inject the key's VALUE, not its name -- an interpolation that resolves to the
  literal string "INFERENCE_API_KEY" makes the proxy reply "LiteLLM Virtual Key
  expected", which the gym wraps as an opaque 500 that reads like a judge
  outage rather than a config error;
- do not set judge_responses_create_params_overrides.model, which collapses the
  panel to a single judge and silently changes the methodology;
- the judge rate-limits before the served model does, so max_concurrent_requests=10
  / concurrency=220 are the golden values to raise only after clean 429 logs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Same treatment the internal eval-config skill got: these are references an
agent acts from, not essays. Cut narrative and cross-file duplication, keep
every fact that prevents a mistake.

reference (293 -> 225):
- Merge the two SIF sections into one. The build-vs-run narrative, the
  privileged/rootless aside and the worked rebuild example were explanation;
  what remains is the rule (prefer a provided SIF, else build on the target
  cluster, never copy between clusters), the silent-fallback warning, the
  arch-aware pre_cmd note, and the diff-the-def check before a rebuild.
- Scoring modes: drop the two-step baseline walkthrough, keep the rubric-vs-
  comparison distinction, the startup error string, and the pointer to the
  conversion checklist.

recipe (143 -> 116): it had grown a second copy of the reference's SIF,
scoring-mode and num_repeats material. Make it the thin entry point it should
be -- what GDPVal is, the standalone rule, the config pointer, canary, and
score extraction (the part that is genuinely unique to it) -- and delegate the
mechanics to the reference in one line.

Verified after the cut that the load-bearing facts survive: the silent-fallback
warning, normalized_elo + results.yml extraction, reference_models, the
sif helper, TRUST_PRE_CMD, num_repeats=1, the LiteLLM key-value trap, the
`test -d` validation gap and the don't-pin-the-judge rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
cjluo-nv added a commit that referenced this pull request Aug 3, 2026
)

### What does this PR do?

Type of change: Bug fix (agent skill documentation)

The `evaluation` skill instructed the agent to **"append `-cu130` to the
image tag"** for NVFP4 checkpoints on Blackwell B300/GB300 (sm_103).
That was correct for v0.19.x, but **vLLM inverted its tag convention at
v0.20.0**: the *unsuffixed* tag is now the CUDA-13 build, and `-cu129`
is the CUDA-12 opt-out.

Consequences of the stale rule:

- `v0.20.1-cu130` / `v0.24.0-cu130` / `v0.26.0-cu130` **do not exist** —
following the rule literally asks for a missing tag.
- The documented fallback (`cu130-nightly-<arch>`) points at ~v0.20-era
builds that are *older* than several models' documented minimum vLLM
version, so it can't serve as an escape hatch either.

This replaces the "append a suffix" instruction with a version-keyed
table plus the durable check: **select a tag whose config blob reports
`CUDA_VERSION` ≥ 13**, resolving the child manifest for the platform you
actually deploy on.

While the PR was open the default image was also bumped, and review
surfaced two follow-on corrections. Full contents:

1. **Tag-convention fix** — version-keyed table, `-cu130` fallback
removed.
2. **Default image `v0.19.1` → `v0.26.0`** (latest vLLM release)
everywhere it was pinned: `SKILL.md` Step 3 and the Step 7.5 table,
`example_eval.yaml`, `example_eval_next.yaml`. Version specifics that
the bump made stale or self-contradictory were dropped (the `e.g.
v0.20.0` bump example and the MiniMax-M2.7 `≥0.20.0` anecdote, both now
*below* the default; the failure-mode lesson is kept).
3. **Convention boundary corrected to v0.20.0** — the first draft said
`≤ v0.20.x` suffixed / `≥ ~v0.21` unsuffixed. Off by a minor release in
both rows; see Testing.
4. **Config blob resolved per deployment platform** — the check said
"arm64 child". GB300/Grace is arm64, but plenty of B300 deployments are
`linux/amd64`.
5. **Same corrections applied to the `deployment` skill**, which carried
the original append rule untouched: its NVFP4 note,
`references/support-matrix.md`, `references/benchmarking.md`, and the
`:latest` pins in `references/setup.md` (now `v0.26.0`, matching the
evaluation skill's never-`:latest` stance).

An earlier revision of this branch also carried a
`.claude/skills/benchmark-model-kernels` symlink, added automatically by
`tools/precommit/sync_claude_skills.sh` — it repairs missing symlinks
repo-wide on any touch of `.agents/skills/`, and #1980 landed that skill
without its link. It has been dropped from this branch to keep the scope
on the vLLM image guidance. Worth its own one-line PR: without the
symlink, Claude Code doesn't load that skill at all.

### Usage

```bash
# Durable check — resolve the child manifest for YOUR platform (arm64 for
# Grace/GB300, amd64 for x86) and read CUDA_VERSION from its config blob:
#   v0.19.1            -> CUDA_VERSION=12.9.1   (unsuffixed = CUDA 12, old convention)
#   v0.19.1-cu130      -> CUDA_VERSION=13.0.1   (suffixed = CUDA 13, old convention)
#   v0.20.0            -> CUDA_VERSION=13.0.2   (transition release: ships both suffixes)
#   v0.26.0            -> CUDA_VERSION=13.0.2   (unsuffixed = CUDA 13, new convention)
#   v0.26.0-cu129      -> CUDA_VERSION=12.9.1   (suffixed = CUDA 12, new convention)
```

### Testing

Verified empirically against the Docker registry API for
`vllm/vllm-openai` — resolved each tag's child manifests and read
`CUDA_VERSION` / `TORCH_CUDA_ARCH_LIST` from the config blob.

**Where the convention flips (arm64):**

| release | unsuffixed | `-cu130` | `-cu129` |
|---|---|---|---|
| v0.18.0 | 12.9.1 | 13.0.1 | absent |
| v0.19.0 / v0.19.1 | 12.9.1 | 13.0.1 | absent |
| **v0.20.0** | **13.0.2** | 13.0.2 | 12.9.1 |
| v0.20.1 | 13.0.2 | **absent** | 12.9.1 |
| v0.20.2 | 13.0.2 | **absent** | 12.9.1 |
| v0.21.0 … v0.26.0 | 13.0.2 | absent | 12.9.1 |

v0.20.0 is the transition release — it publishes both suffixes *and* its
unsuffixed tag is already CUDA 13. That duplication is what hid the
boundary: confirming `v0.20.0-cu130` exists reads as "old convention
still applies at 0.20", while `v0.20.1-cu130` and `v0.20.2-cu130` don't
exist at all.

**Why the platform matters.** `CUDA_VERSION` is identical across
children on every tag checked (v0.26.0, v0.26.0-cu129, v0.20.0, v0.19.1,
v0.19.1-cu130, kimi-k3), but `TORCH_CUDA_ARCH_LIST` is not:

```
v0.26.0  amd64  7.5 8.0 8.6 8.9 9.0 10.0 12.0
v0.26.0  arm64  8.0 8.7 8.9 9.0 10.0 11.0 12.0
v0.19.1  amd64  7.0 7.5 8.0 8.9 9.0 10.0 12.0
v0.19.1  arm64  8.7 8.9 9.0 10.0+PTX 12.0
```

`11.0` appears only on arm64, `7.5` / `8.6` only on amd64 — so the arch
check has to read the child you'll actually run.

**Default bump.** The `0.26.0` family is `{,-aarch64,-x86_64} ×
{,-cu129} × {,-ubuntu2404}` — 12 tags, `cu129` the only CUDA axis, no
`-cu130`. The new default is therefore already a CUDA-13 build, and
NVFP4 on B300/GB300 needs no suffix at all.

Docs-only change; no runtime code touched. `pre-commit` clean.

### Before your PR is "*Ready for review*"

- Is this change backward compatible?: ✅
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: N/A (agent skill
documentation)
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
N/A
- Did you get Claude approval on this PR?: ❌ (not yet run)

### Additional Information

Split out of the GDPVal skill work (#2039) because it is independent of
GDPVal and applies to every NVFP4-on-Blackwell deployment the skill
generates. Now spans both the `evaluation` and `deployment` skills, all
under `.agents/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
  * Updated deployment and evaluation guidance to use vLLM v0.26.0.
* Clarified CUDA image-tag conventions, including CUDA 13 defaults and
CUDA 12 opt-outs.
* Added validation guidance for resolved CUDA versions, platform
architecture settings, and image compatibility.
* Updated NVFP4 Blackwell B300/GB300 support notes, including required
`sm_103` kernel availability.
* Refreshed example recipes, setup commands, benchmarking notes, and
serving-image requirements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n regen test

Regenerating a config from the compressed docs alone reproduced every
score-determining setting exactly, but surfaced three defects. All three fail
silently, which is why they survived earlier passes.

1. SKILL.md's GDPVal branch still named `python-3.12.gdpval.sif` while the
   template, recipe and reference had all moved to 3.13 -- and a stale 3.12 SIF
   in an otherwise-correct dir passes NEL's `test -d` preflight, so the agent
   drops to unsandboxed code-exec with no error. The same step also presented
   *building* a SIF as mandatory, contradicting the prefer-a-provided-SIF rule
   the other two files state. Now: prefer provided, build as fallback, verify
   the filename with `gdpval-sif.sh --check`.

2. The template shipped
   `++...judge_responses_create_params_overrides.model=${gdpval_judge.model}`.
   Harmless in rubric mode (one judge), but on conversion to comparison it
   collapses the 3-member judge panel to a single judge and silently changes
   the scoring methodology. A "splice these overrides in" conversion checklist
   structurally cannot catch a line that must be *removed*, so remove it from
   the template instead: the judge model is already set by
   `openai_model=${gdpval_judge.model}`.

3. The template carried the generic `--max-num-seqs = ceil(parallelism / DP)`
   rule inline. On the gym path `parallelism` is gym-internal, not an in-flight
   request count, so that yields 4096. The correction existed only in the
   reference; put the right rule (`ceil(stirrup_agent.concurrency / DP)`) in the
   artifact people actually edit.

Also makes the score-extraction snippet's `results.yml` path explicit instead of
a bare filename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>

@meenchen meenchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.

Docs/skill-only change that correctly reuses the existing aa_next/-style "branch" convention (recipe + shared reference + example dir + .agents/scripts helper), so the design question is largely settled by precedent — no competing in-repo system is introduced. Tests are legitimately N/A (no test harness exists for .agents/skills, and the license pre-commit hook only covers python/shell).

Issues found:

  1. Contradictory num_repeats guidancereferences/quantization-benchmarks.md tells the agent to "bump it back to 2 for quant validation", while references/gym-gdpval.md says "Use 1 … Do not carry a =2 into a current run" and recipes/tasks/aa_gym/gdpval.md says both goldens use 1. Quant validation is the use case in this repo, so the agent will hit the contradiction on the default path.
  2. gdpval-sif.sh non-fakeroot fallbackapptainer build refuses to write an existing output file without --force, and the fakeroot attempt usually leaves a partial $tmp, so the elif fallback will most likely fail spuriously.
  3. Score-extraction snippet is comparison-only while the shipped template is rubric-only — the copy-pasteable python3 -c will KeyError on the default config's results.
  4. --env-file flag appears in two places but nowhere else in the skill (which documents set -a && source .env); please confirm 0.2.6 nel run accepts it.
  5. GDPVAL_MAX_TURNS is presented as a container lit: env var while the effective value comes from a submit-time ${oc.env:GDPVAL_MAX_TURNS,250} interpolation — the two can't both be right, and the skill elsewhere warns against ${oc.env:...} for non-exported vars.

Nits: the two new YAML headers use Copyright (c) 2025, NVIDIA CORPORATION. which diverges from the canonical LICENSE_HEADER (2026, "& AFFILIATES") that gdpval-sif.sh matches exactly — worth aligning (no third-party code is vendored, so no legal concern beyond consistency). Also note the AA rule change now makes every "run AA" request additionally require TAVILY_API_KEY, a judge endpoint and a prebuilt SIF; the "let the user opt out" wording is doing a lot of work there. Size (~1000 lines) is fine given it's almost entirely prose.

often multi-node. Generate it alongside the `aa/` config; see
`recipes/tasks/aa_gym/gdpval.md` + `references/gym-gdpval.md`. Thinking mode is
mandatory (non-thinking loses ~86% of pairwise judgements). For low-variance
quant comparisons keep the golden `num_repeats: 2` — the example template

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

This contradicts the other two new docs. references/gym-gdpval.md ("## num_repeats") says "Use 1. Both current goldens do … Do not carry a =2 into a current run", and recipes/tasks/aa_gym/gdpval.md says "num_repeats=1 — already set by the template … both current goldens use it". Here the agent is told the golden is 2 and that the template lowered it to 1 "to halve cost", so on the default quant-validation path (the primary use case in this repo) it will override back to ++num_repeats=2 — doubling a multi-hour, judge-metered run against explicit guidance elsewhere. Please pick one story and state it in a single place, with the others pointing at it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — now says 1 everywhere. The reviewed golden uses num_repeats=1; this file was telling the primary quant-validation path to bump back to 2.

Comment thread .agents/scripts/gdpval-sif.sh Outdated
# unprivileged build where fakeroot is unavailable.
if "$APPTAINER_BIN" build --fakeroot "$tmp" "$def_local"; then
:
elif "$APPTAINER_BIN" build "$tmp" "$def_local"; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

The non-fakeroot fallback will probably never succeed: apptainer build errors out when the destination image file already exists unless --force is passed, and a failed --fakeroot attempt commonly leaves a partial $tmp behind. As written, a cluster without an /etc/subuid entry gets _die "apptainer build failed" even though the unprivileged build would have worked. Suggest rm -f "$tmp" (or --force) before the fallback:

if "$APPTAINER_BIN" build --fakeroot "$tmp" "$def_local"; then
  :
else
  rm -f "$tmp"
  if "$APPTAINER_BIN" build "$tmp" "$def_local"; then ...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — clearing $tmp before the unprivileged fallback.

Comment thread .agents/scripts/gdpval-sif.sh Outdated
echo "$sif"; exit 0
fi
printf '\033[31mgdpval-sif: MISSING expected SIF: %s\033[0m\n' "$sif" >&2
[[ -d "$sif_dir" ]] && { echo " dir exists but does not contain it; found:" >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

Under set -e, this [[ -d "$sif_dir" ]] && { … } is a non-final command in the list, so when the dir does not exist the script exits right here and the "Build it with: …" hint on the next line never prints — which is exactly the case where the hint is most useful. Use if [[ -d … ]]; then … fi (or append || true).

Also, inside the block ls -1 … | sed … || echo " (no .sif files)" binds || to the pipeline, and sed exits 0 on empty input, so the "(no .sif files)" branch is effectively dead. Gate on the ls exit status instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — if/then/fi so the "Build it with:" hint prints when the dir is missing, and the (no .sif files) branch is now gated on ls rather than the (always-0) sed.

# See the License for the specific language governing permissions and
# limitations under the License.

# Shared prepare/teardown snippets — include via: defaults: [_gym_prepare, _self_]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

Design question worth answering in the PR body: this file describes itself as "shared … by every benchmark", but there is exactly one gym benchmark and one consumer (example_gym_gdpval.yaml), and the split creates a footgun that then has to be warned about in five separate places (SKILL.md, the recipe, the reference, the config header, the failure-modes list: "if it still hangs, the _gym_prepare.yaml include didn't travel with the config").

Inlining the two shell blocks directly into the task command: would make the example a single self-contained file like example_eval.yaml / example_eval_next.yaml and remove the whole co-location hazard. If the include exists to anticipate future aa_gym/ tasks, please say so explicitly — otherwise the simpler single-file form looks preferable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — inlined both blocks into the task command: and deleted this file. The example is now a single self-contained YAML and the "must travel with the config" warning is gone from all five places it appeared.

# Drop "_cache" if you WANT them uploaded as artifacts.
PERSIST_DELIVERABLES_DIR: lit:/results/gdpval/deliverables_cache
# Stirrup agent turn cap (optional; default 100).
# GDPVAL_MAX_TURNS: lit:100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

GDPVAL_MAX_TURNS is documented three ways that don't agree: here as an optional container env var (lit:100), in references/gym-gdpval.md's env table as lit (optional) … default 100; golden uses 250, but the value the gym actually uses comes from ++…agent_max_turns=${oc.env:GDPVAL_MAX_TURNS,250} in common_params — an OmegaConf interpolation resolved at submit time against the launching shell, not the container env. Uncommenting GDPVAL_MAX_TURNS: lit:100 therefore has no effect on agent_max_turns, and SKILL.md Step 5 explicitly warns against ${oc.env:...} for values that aren't exported with set -a. Please make it one mechanism (either drop the lit: line and document it as a host-side .env value, or hardcode 250 in the override) and fix the reference table to match.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — removed. It could never affect agent_max_turns, which is an ${oc.env:…} resolved at submit time against the launching shell; the golden doesn't set it either.

# workspace, don't copy the yaml alone.
#
# Canary (validates SIF sandbox + judge + gym plumbing on a couple of tasks):
# nel run --config example_gym_gdpval.yaml --env-file .env \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

nel run … --env-file .env doesn't appear anywhere else in the skill — SKILL.md Step 8 and every other example use set -a && source .env && set +a with host: prefixes, and this is the only occurrence of --env-file in the repo. If 0.2.6 nel doesn't have that flag, the canary command as written fails immediately (it's repeated in recipes/tasks/aa_gym/gdpval.md). Please confirm against the installed CLI or switch to the documented source .env form for consistency.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked: --env-file is a real nel 0.2.6 flag (verified against the installed CLI), so leaving it.

python3 -c "
import yaml,sys
m=yaml.safe_load(open('<output_dir>/<run>/nemo_gym.0/artifacts/results.yml'))['groups']['nemo_gym']['metrics']
for k in ('normalized_elo','eval_elo','win_rate'):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

This copy-pasteable snippet hardcodes gdpval_stirrup_agent/comparison/{k}, but the shipped template is rubric-only (gdpval.reward_mode: rubric), so on the default config it will KeyError — and the section above states flatly "The reported GDPVal score is normalized_elo", which is undefined in rubric mode per your own table row. Since rubric is what most users will run first, add the rubric extraction (mean reward over evaluator_rollouts.jsonl) alongside it, or gate the snippet with an explicit "comparison mode only" heading.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the rubric extraction (mean reward over evaluator_rollouts.jsonl) beside the comparison one, each labelled by mode, so the shipped rubric-default config no longer KeyErrors.

cjluo-nv and others added 4 commits August 3, 2026 18:42
… config

Cross-checked the open review findings against the reviewed golden GDPVal run
(Qwen3.6-35B-A3B). Two of my own claims were wrong; the rest are real bugs.

Contradicted by the golden:
- quantization-benchmarks.md still told the quant-validation path to "keep the
  golden num_repeats: 2 ... bump it back to 2", while the recipe and reference
  say 1. The golden uses 1, so the primary use case in this repo was being told
  to double a multi-hour judge-metered run against guidance elsewhere.
- I had documented that max_new_tokens "does not apply" on the gym path. The
  golden sets it alongside the adapter's params_to_remove, so the Step 3
  model-card lookup applies as normal.

Real defects in code I wrote:
- gdpval-sif.sh: the unprivileged fallback could never succeed, because a failed
  --fakeroot attempt leaves a partial $tmp and apptainer refuses an existing
  destination. Clear it before falling back.
- gdpval-sif.sh --check: `[[ -d ... ]] && { ... }` is a non-final command under
  `set -e`, so when the dir is missing the script exited before printing the
  "Build it with:" hint -- exactly when the hint matters. The "(no .sif files)"
  branch was also dead, since sed exits 0 on empty input.
- The template exposed GDPVAL_MAX_TURNS as a container env var, which cannot
  affect agent_max_turns: that value is an ${oc.env:...} resolved at submit time
  against the launching shell. The golden does not set it either.
- The score-extraction snippet was comparison-only, but the shipped template is
  rubric-only, so it KeyErrors on the default config. Added the rubric form.

Checked and NOT changed: `--env-file` is a real nel 0.2.6 flag; and both
`use_reasoning` and `process_reasoning_traces` exist in the installed adapter
config, so the template's field is valid even though the golden uses the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
…lf-contained

The include was described as "shared by every benchmark", but there is one gym
benchmark and one consumer, and the split created a footgun that then had to be
warned about in five separate places -- SKILL.md, the recipe, the reference, the
config header, and the failure-modes list ("if it still hangs, the include
didn't travel with the config"). Copying the yaml without its sibling silently
produced an unresolvable ${gym_prepare.*} interpolation.

Inline both shell blocks into the task `command:` and delete the file. The
example is now a single self-contained yaml like example_eval.yaml and
example_eval_next.yaml, and the "copy the whole dir" instruction disappears from
every doc that carried it.

The reference keeps the *why* under a "Gym prepare / reap" heading, since the
blocks are still unobvious: the venv repair works around the eval image's
deployment-oriented packaging, and the setsid + process-group reap works around
Gym's incomplete shutdown (orphaned Ray workers otherwise hold the launcher's
stdout open and the run hangs in post-eval). Both are marked for removal once
upstream fixes land.

Verified: the template parses, the inlined command passes `bash -n` with
quote-containing params substituted, and no `#` leaked into a folded scalar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
A regen from the now-self-contained template reproduced every score-determining
setting and copied the inlined `command:` verbatim, which was the point of the
inlining. Two things it surfaced are worth fixing.

- The prepare block hardcoded `export UV_CACHE_DIR=/opt/cache/uv`, overriding the
  `UV_CACHE_DIR` env var and the `/cache/uv` mount that the template sets up two
  screens earlier -- so the mounted uv cache was silently bypassed for the venv
  repair, and `++uv_cache_dir=$UV_CACHE_DIR` then pointed at a container-local
  dir. Only set it when it is not already provided. (No `${VAR}` braces --
  OmegaConf parses those.)
- Document the one case where the SIF <-> gym-commit coupling rule does not
  apply: a site-provided SIF used with a site-provided gym image. Those images
  bake Gym as a non-git dir, so the pin is inert and the two artifacts are
  already matched -- even though the SIF filename can encode a different SHA
  than `install_on_the_fly.commit`, which reads like a violation of the rule
  stated just above it.

Not changed: a review claim that MLflow auto-export is rejected on aws-cmh
because `sbatch_extra_flags.qos` reaches the export sbatch. Evidence says
otherwise -- the export job ran on partition `cpu` and COMPLETED (2m20s),
precisely because those flags are not propagated to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
…re block

The prepare block overwrote PYTHONPATH outright, discarding anything the base
image or launcher had set. Prepend instead. The usual
"${PYTHONPATH:+:$PYTHONPATH}" idiom cannot be used here because OmegaConf parses
${...} in the surrounding YAML, so this uses a plain $VAR test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@cjluo-nv

cjluo-nv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — worked through all of these. Summary of what changed, and what I deliberately didn't change.

The four CodeRabbit inline comments were made against 0c6a105b2 (the first commit); the branch is now 13 commits in, so several were already resolved by later work. Marking those as such rather than silently ignoring them.

Fixed

Finding Resolution
gdpval-sif.sh: SIF commit/filename defaults stale vs the template Already fixed after that review — defaults are now dd41196f… / python-3.13.gdpval.sif, matching the template pin.
gdpval-sif.sh: unprivileged fallback can't succeed if --fakeroot leaves $tmp Real bug, fixed: rm -f "$tmp" before the fallback build. Our successful build only worked because fakeroot failed before creating the file.
gdpval-sif.sh --check: [[ -d … ]] && { … } under set -e Real bug, fixed with if/then/fi. The script exited before printing the "Build it with:" hint — exactly when it's most useful. The (no .sif files) branch was also dead (sed exits 0 on empty input); now gated on ls.
pre_cmd hardcodes an amd64 apptainer .deb Fixed: installs via the Ubuntu PPA (arch-aware). aarch64 is most Blackwell/Grace clusters, so the hardcoded deb simply could not install.
PYTHONPATH overwritten, discarding inherited value Fixed: prepend instead. Note ${PYTHONPATH:+…} can't be used — OmegaConf parses ${…} in the surrounding YAML — so it uses a plain $VAR test.
GDPVAL_MAX_TURNS documented three inconsistent ways Fixed: removed the container env var. It could never affect agent_max_turns, which is an ${oc.env:…} resolved at submit time against the launching shell. The golden doesn't set it either.
num_repeats contradiction in quantization-benchmarks.md Fixed. The reviewed golden uses 1; that file was telling the primary quant-validation path to bump back to 2, i.e. to double a multi-hour judge-metered run against guidance in the other two docs.
Score-extraction snippet is comparison-only but the template ships rubric Fixed: added the rubric form (mean reward over evaluator_rollouts.jsonl) beside it, each labelled by mode.
Design: _gym_prepare.yaml split is a footgun for one benchmark / one consumer Agreed and done — inlined into the task command: and deleted the file. The example is now a single self-contained YAML like example_eval.yaml, and the "must travel with the config" warning is gone from all five places it appeared.

Not changed, with reasons

  • --env-file .env — this is a real nel 0.2.6 flag (verified against the installed CLI), not a typo for the source .env form.
  • || true on the squashfuse/fuse3 install — deliberate. Those are optional: without them apptainer falls back to per-call extraction (slower, still sandboxed). The apptainer install itself has no || true and fails loudly, which is the case that actually matters. The perf caveat is documented in the reference.
  • MLflow auto-export being rejected on aws-cmh — evidence says otherwise. The export job ran on partition cpu and COMPLETED in 2m20s, because sbatch_extra_flags is not propagated to the export sbatch. Leaving the docs as-is.

Validation

The skill was exercised end-to-end rather than just proofread: a GDPVal comparison-mode run on an aarch64 GB300 cluster completed and produced a real score (normalized_elo), and after each round of edits a subagent regenerated the config from the skills alone — blocked from seeing the known-good answer — and the output was diffed against it. All score-determining settings (9 references + ELOs, both mount maps, container, SIF, reward_mode, num_comparison_trials, judge_sampling_seed, judge auth, both throttles, num_repeats, multistage shape) matched every time, including after the inlining, where the agent copied the inlined command: verbatim.

@sugunav14 sugunav14 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two line-anchored findings below. Separately, the SIF helper's flock/double-check/atomic-mv handling and the test -d vs -f validation-gap analysis are solid, and the python-3.13 filename + dd41196f... gym pin are now consistent across all five files that reference them.

Lower-severity items (no cleanup trap in gdpval-sif.sh, --check testing the local FS rather than the cluster, -f-only SIF validation, unscoped pkill -f raylet, unpinned apptainer PPA in pre_cmd, GDPVAL_MAX_TURNS missing from env.example) are in my earlier comment on this PR.

# Writable staging dir for ref files (bind-mounted, see execution).
mkdir -p /gdpval_ref_files

ng_prepare_benchmark {{config.params.extra.nemo_gym.data_prep_params}} {{config.params.extra.nemo_gym.common_params}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set -ex here puts HF_TOKEN / INFERENCE_API_KEY / TAVILY_API_KEY in plaintext in the eval log.

The task command: opens with set -ex (L244), and these two param blobs expand to +hf_token=$HF_TOKEN (data_prep_params) and, in common_params:

++gdpval_judge_model.responses_api_models.openai_model.openai_api_key=$INFERENCE_API_KEY
++gdpval_stirrup_agent.responses_api_agents.stirrup_agent.tavily_api_key=$TAVILY_API_KEY

set -x traces commands after expansion, so the + ng_prepare_benchmark ... trace line carries all three secret values. They land in the eval log on shared FS, and export.mlflow.log_logs: true (L369) then uploads that log to MLflow.

What suggests an oversight rather than an accepted tradeoff: the rollout call at L297 passes the same common_params, but it's written into /tmp/gym_run.sh via the quoted heredoc (L296-299) and run by a shell that never sets -x -- so $INFERENCE_API_KEY stays literal and is expanded only at run time, never traced. The right pattern is already there one line down; this call just isn't covered by it.

Suggested change
ng_prepare_benchmark {{config.params.extra.nemo_gym.data_prep_params}} {{config.params.extra.nemo_gym.common_params}}
set +x
ng_prepare_benchmark {{config.params.extra.nemo_gym.data_prep_params}} {{config.params.extra.nemo_gym.common_params}}
set -x

(The set -ex at L171 in pre_cmd is fine -- nothing on those lines carries a secret.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 4bc6937d9 — thank you, this was a real leak, not a tradeoff.

I checked a completed run's client log rather than reasoning about it, and the traced line carries the expanded value:

hf_token=$HF_TOKEN     <- what the config says
hf_token=hf_TOBgm...   <- what the log actually contains

Both HF_TOKEN and INFERENCE_API_KEY were present. Applied your suggestion (set +x / set -x around that one call). Your read of why it was an oversight is exactly right: the rollout call below already gets this for free via the quoted heredoc, and this call simply wasn't covered by the same pattern.

Beyond the code fix, the already-completed runs need remediation — those logs went to MLflow via export.mlflow.log_logs: true, so the affected keys are being rotated and the uploaded artifacts purged.


```bash
nel run --config example_gym_gdpval.yaml --env-file .env \
-o ++evaluation.nemo_evaluator_config.config.params.limit_samples=2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is limit_samples actually a canary on the gym path?

The PR description lists, among traps found during the validated run:

limit_samples not limiting gym rollouts

But limit_samples=2 is still presented as the canary in three places -- here, SKILL.md (GDPVal branch, step 4), and the example_gym_gdpval.yaml header -- and that trap appears nowhere in the final docs (no occurrence of limit_samples in references/gym-gdpval.md). It reads like the finding was dropped in the 436->341-line compression pass while the instruction depending on it survived.

If the trap is real, someone following this step launches the full 220-task multi-hour run believing it's a two-task smoke test -- inverting the purpose of the step, on the heaviest benchmark in the suite. Worth either documenting how to actually bound a gym canary, or stating plainly that limit_samples is inert here and describing what the canary is instead (e.g. watch the first rollouts for the SIF-fallback warning and judge auth, then cancel).

I couldn't verify this one empirically -- if limit_samples does work on the gym path, disregard.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — limit_samples is inert on the gym path, and your inference about how it happened is right: it was observed during the validated run (the "limit_samples=2 canary" ran all 220 tasks), made the PR description, and was then lost in the 436->341-line compression while the three instructions depending on it survived.

Fixed in 4bc6937d9 in all three places (recipe, SKILL.md GDPVal branch, template header). The gym does its own data prep and rollout collection, so the launcher-level limiter never reaches it.

Since there is no cheap sample-limited canary, the docs now describe the real procedure instead: launch, and treat the first ~20-30 minutes as the canary, with the greps to run —

grep -c "Using Apptainer container"   $RD/logs/client-*.log        # sandbox actually used
grep -c "falling back\|not a git repo" $RD/logs/client-*.log       # unsandboxed / inert pin
grep -ciE " 401 | 403 |Internal Server Error" $RD/artifacts/nemo_gym_logs/gdpval_judge_model.log
wc -l $RD/artifacts/evaluator_rollouts.jsonl                       # rollouts flowing

— plus a note that in comparison mode stage 1 (45 tasks) is a natural early checkpoint, since an ELO estimate lands before the 220-task stage 2 begins.

cjluo-nv and others added 2 commits August 3, 2026 20:51
…ake canary

Two findings from review, both confirmed against a real run.

1. Secret leak. The task `command:` opens with `set -ex`, and `set -x` traces
   commands AFTER expansion -- so the `ng_prepare_benchmark` trace line wrote
   $HF_TOKEN, $INFERENCE_API_KEY and $TAVILY_API_KEY in plaintext into the eval
   log, which lives on shared FS and is uploaded to MLflow when
   export.mlflow.log_logs is true. Verified in a completed run's client log: the
   traced line carries the expanded token, not `$HF_TOKEN`.
   Wrap that one call in `set +x` / `set -x`. The rollout call below it was
   already safe -- it goes through the quoted heredoc, so its secrets stay
   literal until a shell that never sets -x expands them; this call just wasn't
   covered by the same pattern.

2. `limit_samples` is inert on the gym path -- the gym does its own data prep and
   rollout collection, so the launcher-level limiter is ignored and you get the
   full 220-task run. This was observed during the validated run but the finding
   was lost in the doc-compression pass while the instruction depending on it
   survived in three places, so following the documented canary launches the
   heaviest benchmark in the suite believing it is a two-task smoke test.
   Replaced with the real procedure: launch, then treat the first ~20-30 minutes
   as the canary (SIF-sandbox line, judge auth, rollouts flowing), with the
   grep commands to check; note stage 1 of multistage as a natural checkpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Lower-severity items raised alongside the secret-leak and canary findings.

gdpval-sif.sh:
- Add an EXIT trap so an interrupted or failed build leaves no `.build.*` /
  `.def` leftovers in the shared SIF dir. ($tmp is renamed on success, so the
  trap only ever removes leftovers.)
- `--check` validated with `-f` only, which passes on a truncated or 0-byte
  file -- exactly what an interrupted copy leaves behind, and the failure it is
  supposed to prevent is silent. Now also requires a plausible size (>100 MB;
  a real GDPVal SIF is 1-4 GB) and, when apptainer is available, that
  `apptainer inspect` succeeds. Verified on-cluster: a real SIF passes, a
  0-byte one is rejected with a rebuild hint.
- Document that `--check` inspects the filesystem it RUNS ON, so it must be run
  on the cluster (srun/ssh) rather than the submitting box.

template:
- Scope the Ray reap to our own uid. `pkill -9 -f raylet` matches every raylet
  on the node, including other users'/jobs' daemons on a shared node.
- Note that the apptainer PPA install is deliberately unpinned (it is the only
  source with current arm64 builds; pinning risks unavailability) and that the
  resolved version is echoed into the run log for reproducibility.

env.example:
- Document `GDPVAL_MAX_TURNS`. It is read at SUBMIT time from the launching
  shell via ${oc.env:...}, so it belongs with the other exported values; setting
  it as a container env var has no effect (that trap was removed earlier).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@cjluo-nv

cjluo-nv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@sugunav14 — both line-anchored findings were real and are fixed in 4bc6937d9; the lower-severity list from your earlier comment is fixed in 507007b56. Details on each thread, summary here.

The two findings. The set -x one was a genuine leak, not an accepted tradeoff — I verified it against a completed run's log rather than reasoning about it, and the trace line carries the expanded HF_TOKEN / INFERENCE_API_KEY. Applied your suggested set +x / set -x. Separately, the affected keys are being rotated and the MLflow-uploaded logs purged, since log_logs: true had already shipped them. The limit_samples one was exactly as you guessed: observed during the validated run, reached the PR description, then lost in the compression pass while the three instructions depending on it survived. Now documented as inert, with the real canary procedure in its place.

Lower-severity items (507007b56):

Item Resolution
No cleanup trap in gdpval-sif.sh Added an EXIT trap so an interrupted build leaves no .build.* / .def leftovers in the shared SIF dir.
-f-only SIF validation Real gap — -f passes on a 0-byte file, which is what an interrupted copy leaves, and the failure it guards against is silent. Now also requires a plausible size (>100 MB) and a successful apptainer inspect where available. Verified on-cluster: real SIF passes, 0-byte one is rejected with a rebuild hint.
--check tests the local FS Documented that it inspects the filesystem it runs on, so it must be run on the cluster (srun/ssh), not the submitting box.
Unscoped pkill -f raylet Scoped to our own uid — the unscoped form matches every raylet on a shared node, including other jobs'.
Unpinned apptainer PPA Kept unpinned deliberately (it is the only source with current arm64 builds, and pinning risks unavailability), but documented the reasoning and the resolved version is echoed into the run log.
GDPVAL_MAX_TURNS missing from env.example Added, with the reason it belongs there: it is read at submit time from the launching shell via ${oc.env:...}, so the container-env form has no effect.

Thanks for the careful pass — the two anchored findings were the highest-value review input this PR has had, and both were things the end-to-end run had already brushed past without surfacing.

@sugunav14 sugunav14 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

After the last compression pass the two markdown files held at ~365 lines, but
the template had grown to 391 with 168 comment lines against 214 config lines --
each review fix added a three-to-six line explanation next to the line it fixed.

Cut the prose the recipe and reference already own (the 32-line header is now 16)
and reduce eight multi-line comment blocks to one or two lines each. What stays
is the part that prevents an edit mistake *at that line*: never `#` inside the
folded scalar, mount keys are not interpolated, `set +x` around the traced call,
the api_key is a VALUE not a NAME, plain `$VAR` only because OmegaConf parses
`${...}`, and the uid-scoped pkill. The reasoning behind each moves to
references/gym-gdpval.md, which is where a reader already is.

Verified after the cut: the YAML parses, the inlined command still passes
`bash -n` with quote-containing params substituted, no `#` leaked into any
folded scalar, and all nine load-bearing markers are still present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@cjluo-nv

cjluo-nv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up: compressed the template in 3f8c7dd5f391 → 360 lines, comments 168 → 137.

Context for why this was worth doing separately: the two markdown files have held at ~365 lines since the earlier compression pass, but the template kept growing, because each review fix in this PR added a three-to-six line explanation immediately next to the line it fixed. That is the right instinct while fixing, and the wrong end state — the file people copy ended up with more comment than config.

The rule I applied: the template keeps whatever prevents an edit mistake at that line; the reasoning moves to references/gym-gdpval.md, which is where a reader has already been sent. So these stay, in one or two lines each:

  • never put # inside the folded (>-) scalar — it folds to one line and comments out every override after it
  • mount keys are never interpolated, so use the literal path
  • set +x around the traced call, or the params' secrets land in the log
  • api_key is injected as a VALUE, not a name
  • plain $VAR only, because OmegaConf parses ${...}
  • the uid-scoped pkill

and the six-line explanations behind each are gone from the yaml.

Verified after the cut, not just eyeballed: the YAML parses, the inlined command: still passes bash -n with quote-containing params substituted, no # leaked into any folded scalar, and all nine load-bearing markers are still present.

Running totals for the docs in this PR: reference 293 → 236, recipe 143 → 129, template 391 → 360.

@cjluo-nv
cjluo-nv merged commit 14b20c0 into main Aug 3, 2026
44 checks passed
@cjluo-nv
cjluo-nv deleted the chenjiel/gdpval-eval-skill branch August 3, 2026 22:10
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-03 22:10 UTC

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.

3 participants