Skip to content

Merge main into gdpval-devel - #2923

Open
agronskiy wants to merge 197 commits into
gdpval-develfrom
agronskiy/gdpval-devel-with-main
Open

Merge main into gdpval-devel#2923
agronskiy wants to merge 197 commits into
gdpval-develfrom
agronskiy/gdpval-devel-with-main

Conversation

@agronskiy

@agronskiy agronskiy commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Brings gdpval-devel up to date with main — but deliberately not with main's tip.

What this now merges, and why

This branch merges main at b793621 ("Super 3.5 vllm model tuning (Qwen3.5-122B-A10B, Inkling-Small)", #2599), the commit immediately before 65129dd ("chore(deps): pin openai to 2.44.0", #2456). An earlier revision of this PR merged the tip of main; that has been rebuilt.

#2456 added

class NeMoGymChatCompletionCreateParamsNonStreaming(BaseModel):
    model_config = ConfigDict(extra="forbid")

which makes the proxy reject every agentic chat-completions request carrying chat_template_kwargs — the field agentic GDPVal runs use to drive thinking budgets on self-hosted models. Every such request comes back as

422 Unprocessable Entity
{"detail":[{"type":"extra_forbidden","loc":["body","chat_template_kwargs"],
 "msg":"Extra inputs are not permitted","input":{"thinking":true}}]}

so the rollout dies before it reaches the model. Merging past 65129dd would import that regression into the branch the production GDPVal runs are pinned to.

b793621 is the last commit that is free of the regression while still giving the branch everything else it needs:

  • requires-python = ">=3.13.14", so the py313 runtime container stays correct;
  • extra="forbid" appears exactly once in nemo_gym/openai_utils.py — on NeMoGymResponseCreateParamsNonStreaming (the pre-existing 2025 Responses-class server-side validation) and not on the ChatCompletion class;
  • the rollout-observability work (bc521f7) is an ancestor.

Conflicts

Eight files conflicted; every GDPVal file auto-merged. Each conflict was resolved to keep both sides' behaviour rather than pick a winner.

get_venv_path and resolve_server_venv_path turned out to be the same function under two names (byte-identical bodies), canonicalised on devel's name and docstring with main's Path(...) normalisation, main's call sites and the monkeypatch.setattr in test_cli.py renamed, and the now-unused ROOT_DIR import dropped. openai_utils.py took main's content-part types — its video_url part accepts Union[str, Dict[str, Any]] where devel's was dict-only, and it adds File — while keeping devel's comment recording the 422 that motivated adding a video part at all. rollout_collection.py now carries main's exporters (upload_rollouts/export_rollouts/get_exporters, replacing the W&B-specific path) and token-capture retirement alongside devel's dispatch budget, drain margin, DispatchLatencyTracker, kill_shaped no-persist rows, _validate_dispatch_concurrency and ordered_tasks. rollout_reverification.py is a plain union of both import lists, dropping devel's NG_TERMINAL_KEY as NG_TERMINAL_KEY re-export idiom.

Three resolutions are semantic rather than textual and are worth a reviewer's eye:

  1. Main's from time import time shadows the time module that devel's time.monotonic() calls need. The module import was kept and main's bare time() calls rewritten as time.time() — taking main's side verbatim compiles and then fails at runtime.
  2. The persistence branch takes main's flat if no_persist / elif failure_class is not None / else chain with its token-capture retirement, dropping devel's result_strs.append (it fed the removed W&B table) and the stale "not the W&B table either" wording in the kill_shaped comment.
  3. Devel's ordered_tasks = [asyncio.ensure_future(...)] scheduling was kept, because main still passes bare coroutines to as_completed, which makes dispatch_longest_first a no-op, and the shared code after the conflict references ordered_tasks.

Fresh-run cleanup in run_from_config now unlinks both the failures sidecar and the aggregate-metrics file, so a re-run cannot inherit retry attempts or published metrics from an older run at the same output path; test_rollout_collection.py keeps devel's version of that test, which is a superset of main's.

Two resolutions changed relative to the previous (main-tip) revision of this PR, because the older merge target predates the code they applied to. local_vllm_model/setup.py: both sides pin vllm==0.24.0 here, so there is no 0.24.0-vs-0.25.1 decision to make — the file is main's (which also pins flashinfer-python==0.6.12) plus devel's note that 0.24.0 is the first release with MiniMax-M3 support. And the guard on main's "None of the N dispatched rollouts produced a result" RuntimeError, plus the two test fixes for main's run_examples agent-name validation and its _ng_rollout_latency_ms attachment, are all unnecessary: none of that code exists at b793621.

Verification

ruff check and ruff format --check with the CI-pinned ruff 0.9.9 are clean across nemo_gym, tests, responses_api_models and resources_servers. (Newer local ruff builds report drift in files inherited unchanged from main; under the pinned version there is none, so this revision carries no ruff-cleanup commit.)

resources_servers/gdpval/tests: 386 passed, 11 skipped — the reference_missing path (#2796) and strict_comparison_trials (#2807) are both intact.

tests/unit_tests against an origin/main baseline built in a scratch worktree on the same environment: 79 failures on the baseline, 101 here. The 44 new ones are all in test_openai_utils.py (43) plus test_responses_api_model_streaming.py::test_prunes_nested_extra_fields, and are an environment artefact rather than a merge defect — b793621 predates #2456 and still declares openai<=2.7.2, while the venv has openai==2.44.0 installed. Those tests enumerate the installed SDK's item tags (shell_call, apply_patch_call, compaction, tool_search_call, …) and require a Gym union member for each; the pre-2.44 schemas in this tree have none. Re-running the same suite with an SDK inside the declared pin (2.7.2 shadowed onto PYTHONPATH, venv untouched) gives 1 failed / 2976 passed, and that one failure — test_opensandbox_cleanup.py::test_script_help_runs_by_direct_path — is also present in the origin/main baseline. The new-failure set against baseline is therefore empty, and no failure touches rollout collection, reverification, the CLI or GDPVal.

🤖 Generated with Claude Code

kajalj22 and others added 30 commits August 4, 2026 12:04
## Summary
- Bumps \`mlflow\`, \`mlflow-skinny\`, and \`mlflow-tracing\` from
\`3.14.0\` to \`3.15.1\`
- Pins \`stirrup==0.1.12\` to block the coincidentally-released 0.2.0
which broke \`ChatCompletionsClient\` (unrelated to the mlflow bump, but
fixed here to unblock CI)
- Updates \`uv.lock\` accordingly

## Test plan
- [ ] CI green

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
…r wiring. (#2297)

## Summary
- Add a Configure Models page for the default `POST /v1/messages`
dialect on every Gym model server, and how to wire `claude_code_agent`
through `model_server` (or call Anthropic / another Messages host
directly).
- Link the page from the model-server index, Integrate Existing Agents,
release notes, and the Claude Code agent README.

Closes #1476

## Test plan
- [ ] `cd fern && npm run check`
- [ ] Spot-check `/main/model-server/anthropic-messages` in Fern preview
- [ ] Confirm cards/links from Configure Models and Integrate Existing
Agents resolve

Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
Follow-up to #2163, the unpaired call guard now makes one pass over the
output items instead of two comprehensions.

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

Signed-off-by: adil-a <adil.asif2000@hotmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary

The kilocode agent called its model provider directly, so its requests
and responses never reached Gym. Model calls went uncaptured, the
provider block had to be rewritten per backend, and there was no path to
token IDs or logprobs.

This adds `model_server` to `KiloCodeAgentConfig`. When set, the agent
writes a `nemo` provider into `kilo.json` pointed at that server and
passes `-m nemo/<model>`, so one config runs against vLLM, OpenAI, or an
inference provider by swapping `--model-type`. Setting `model_server:
null` keeps the previous behaviour of calling a provider declared in
`kilo_config`.

## What changed

- `model_server` wiring: `_write_kilo_config` becomes build-then-write.
It used to return early when `kilo_config` was empty, which would have
left a model-server-only run with no provider at all. `_build_command`
and `_env` now prefer the resolved URL, so a config carrying both a
model server and `openai_base_url` does not point the subprocess
environment at the provider the `nemo` provider is meant to replace.
- Per-rollout capture: During `/run` the base URL carries the
`/ng-rollout/<id>` prefix, using the base class's `url_path_for_run` and
`resolve_model_base_url` rather than a local reimplementation. Captured
model calls are attributable to the rollout that made them.

## On the model limits

`context_window` and `max_output_tokens` set kilo's `limit`. The
defaults are sized for a 32k-window model server rather than copied from
`opencode_agent`.

Kilo's system prompt and tool definitions run to ~10k tokens, so a large
output budget pushes `prompt + max_tokens` past `max_model_len`. vLLM
rejects that with a 400, which the Gym model server converts into an
empty completion with `finish_reason: length` rather than an error
(`vllm_model/app.py`, `is_out_of_context_length`). The run then produces
no assistant message and scores zero, with nothing in the CLI's output
to say why.

Two related notes:

- There is no truncating default to guard against. An unlisted model
gets `limit.output: 0`, and kilo's `min(limit.output, 32000) || 32000`
falls back to 32000. The hazard is a budget that is too *large*, not too
small.
- `reasoning_field` sets `interleaved.field`. Kilo turns interleaved
reasoning off for custom openai-compatible providers unless the field is
named (its built-in default only applies it to model ids containing
"deepseek"), so without this the reasoning channel is dropped. It
defaults to `reasoning_content`, which is what Gym model servers write;
vLLM >= 0.16 also sends `reasoning`, which is why it is configurable
rather than hardcoded.

Kilo splits `-m` on the first `/` only (`let [A, ...L] = H.split("/")`),
so a slashed model name such as `nemo/Qwen/Qwen2.5-7B-Instruct` resolves
to the model `Qwen/Qwen2.5-7B-Instruct` under the provider `nemo`.
Verified against the shipped binary and live.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…2317)

`fern/versions/main.yml` puts `training-tutorials` and
`evaluation-tutorials` in the same `section: Tutorials`, so both publish
under `/tutorials/`. Evaluation tutorials are already linked that way.
Training tutorials are not.

Against the published site:

```
/nemo/gym/tutorials/training-tutorials/nemo-rl-grpo/   200
/nemo/gym/training-tutorials/nemo-rl-grpo/             308 -> /tutorials/training-tutorials/nemo-rl-grpo
/nemo/gym/tutorials/evaluation-tutorials/evalplus/     200
/nemo/gym/evaluation-tutorials/evalplus/               404
```

The old training form works, but only because Fern redirects it. The
evaluation form has no such redirect. Having two sibling folders under
one section linked two different ways makes the prefix look optional,
and it isn't

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
`ng_viewer` was removed in #726 and swept from the READMEs in 490138d,
but this file had a second occurrence that was missed. Fixes defect C.

Signed-off-by: Ewa Dobrowolska <edobrowolska@nvidia.com>
## Standalone NeMo-Gym container

```
FROM cuda-dl-base:26.03 (ubuntu + CUDA, no NGC pytorch)
        │
        ▼
   [base stage]
   Python 3.12, uv, git
        │
        ▼
  [hermetic stage]
  uv sync --extra vllm --locked
  ├── nemo-gym core (ray, fastapi, aiohttp, mlflow …)
  ├── vllm == 0.26.0
  └── flashinfer-python == 0.6.14  (pre-compiled kernels)
        │
        ▼
  [release stage]
  install nemo-gym project
  optional: NEMO_GYM_PREFETCH_SERVERS → per-server .venvs
  entrypoint: gym
```

**Per-server venvs** (e.g. `local_vllm_model`, `genrm_model`) are
separate isolated environments created by `gym env start` at runtime via
`uv pip install -e .`. Pass `NEMO_GYM_PREFETCH_SERVERS` to bake them in
at build time instead.

## CI

`build-container.yml` runs on `pull_request` and `push` to main (when
`Dockerfile`/`pyproject.toml`/`uv.lock` change) using `ubuntu-latest`.
Validates the Docker build only — no push, no registry.

## Changes

- `docker/Dockerfile` — 4-stage build, vllm + flashinfer locked
- `pyproject.toml` — `[vllm]` optional extra (`vllm==0.26.0`,
`flashinfer-python==0.6.14`)
- `uv.lock` — locked packages
- `local_vllm_model/setup.py`, `genrm_model/setup.py` — flashinfer
pinned alongside vllm
- `.github/workflows/build-container.yml` — build-only CI, triggers on
PR and push to main

## Dispatch

https://github.com/NVIDIA-NeMo/Gym/actions/workflows/build-container.yml

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

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…a flag as its own correction (#2303)

Related to #1434 
Fixes DEFECT A — `-v` / `--verbose` is not global.

Issue: `-v`/`--verbose` was registered only on leaf subparsers, so `gym
-v list benchmarks` failed while the trailing form worked - it is now on
the top-level parser too. The "did you mean?" hint also matched a
misplaced flag against itself; the rejected token is now excluded from
its own candidate set.

Signed-off-by: Ewa Dobrowolska <edobrowolska@nvidia.com>
Co-authored-by: Anwith Kiran <anwithk@nvidia.com>
…v resolve docs example (#2310)

Fix defects E and G. `gym env validate` promised "a clean message, no
traceback" but dumped omegaconf's stack trace when an interpolation
couldn't be resolved; adds `ConfigInterpolationError` and translates the
omegaconf exception at the parsing boundary. Also fixes the `gym env
resolve` docs example, which needed three `++policy_*` keys it never
mentioned - unlike `validate`, `resolve` substitutes no dummy model.

---------

Signed-off-by: Ewa Dobrowolska <edobrowolska@nvidia.com>
## Summary
- Bump Python 3.12 → 3.13.14 and uv 0.11.19 → 0.11.29
- Add `pyarrow>=23.0.1` transitive dep floor
- Fix Python 3.13 compat in server deps (scipy, spacy, audioop-lts)
- Regenerate `uv.lock`

## Test plan
- [x] Unit tests pass
- [x] Server suite passes

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

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
## `gym eval submit` — Slurm/Pyxis orchestration (initial
implementation)

Introduces `gym eval submit`, a new CLI command that takes a declarative
YAML config and submits one sbatch job per benchmark to a Pyxis-enabled
Slurm cluster, handling staging, SSH transfer, service startup, health
checks, and driver execution end-to-end.

### What's in this PR

**Config model (`nemo_gym/orchestration/api.py`)**
- `SubmitConfig` — top-level config with `services`, `compute`,
`driver`, `job` sections; all models use `extra="forbid"` so typos in
YAML surface immediately
- Discriminated unions for `ServiceConfig` (`vllm`, `ray`) and
`ComputeConfig` (`slurm`) — unknown types are rejected at parse time
- `BaseModelServiceConfig` — base for services that can be wired as the
policy model; currently `VllmServiceConfig`
- `NodePool` with structured allocation fields (`nodes`,
`ntasks_per_node`, `gpus_per_node`) the executor uses for deployment
decisions, plus `extra_args` for arbitrary `#SBATCH` directives
- `driver.policy_model` — syntactic sugar: naming a service auto-injects
`policy_base_url`, `policy_model_name`, `policy_api_key` into every
benchmark's `run` args, with conflict detection if those keys are
already set
- `driver.gym_install` — optional `{repo, ref}` to install gym from
source at runtime via `uv`
- `benchmarks` as a dict (name is the key); each benchmark has `prepare`
and `run` dicts of Hydra overrides

**Executor (`nemo_gym/orchestration/executors/`)**
- `BaseExecutor` — thin ABC; `SlurmExecutor` is the only implementation
- `SlurmExecutor` — stages job dirs locally, rsync's to remote, submits
all benchmarks in one SSH session, prints Slurm job IDs on success;
`--dry-run` prints generated scripts without touching the cluster
- `Connection` — `LocalConnection` (shutil + subprocess) vs
`SSHConnection` (ControlMaster socket, single session for copy + all
sbatch calls); dispatched based on whether hostname matches
`socket.gethostname()`
- `build_sbatch_script` — generates a Pyxis sbatch script per benchmark:
`#SBATCH` directives, backgrounded `srun` per service with `--overlap
--no-container-mount-home`, curl-based health checks with dead-process
detection, then a single driver `srun` that optionally runs `gym eval
prepare` before `gym eval run` in the same container step
- Hydra overrides use `+` (not `++`) so `config_paths` participates in
gym's `_merge_config_paths` coalescing and doesn't clobber model-type
configs
- Output artifacts land at
`<remote_bench_dir>/artifacts/rollouts.jsonl`; `logs/` and `artifacts/`
dirs pre-created during staging

**CLI**
- `gym eval submit --config <yaml> [--dry-run]`
- `@experimental` decorator warns users on invocation

### Known gaps / next steps

1. **Local submit (login node)** — `LocalConnection` is wired when
hostname matches, but untested end-to-end
2. **Env var injection** — per-service and driver env vars (literal
values and host env var references) not yet supported
3. **Rollout output path** — `artifacts/rollouts.jsonl` is set but
downstream profiling commands haven't been validated against it
4. **Multi-instance vLLM** — single vLLM instance per job;
multi-instance requires allocation shape reasoning (`gpus_per_node` is
modeled but not acted on yet)
5. **Mounts** — per-service and driver container mounts
(`--container-mounts`) not yet supported
6. **RayService** — placeholder only; needs a real Ray cluster spanning
the full allocation wired into Gym's Ray backend
7. **Multi-node vLLM** — `--distributed-executor-backend` (mp or ray)
not yet generated
8. **Broader benchmark coverage** — only gsm8k tested; more complex
benchmarks (e.g. MCP-Atlas) may surface config or timing issues
9. **Startup parallelism** — currently: start services → wait for health
→ prepare → run; prepare could overlap with service startup to reduce
wall time

---------

Signed-off-by: Tomasz Grzegorzek <tgrzegorzek@nvidia.com>
Signed-off-by: prokotg <19536019+prokotg@users.noreply.github.com>
…verage (#2293)

## Summary

- add a versioned `ng_trajectory` projection for normalized model calls,
token statistics, semantic turns, tool observations, and
invocation-scoped model-visible history
- preserve provider-reported cached, reasoning, and total token usage
across capture, conversion, and multi-turn agent loops
- preserve Responses lifecycle status as `response_status` without
reinterpreting dialect-specific `finish_reason`
- add an opt-in reference trajectory producer for Simple Agent and
document current V/O/X coverage for C1-C7 across all 34 agents

This PR establishes the shared schema and one reference producer. The
capability matrix reports current support; it does not claim that every
producer satisfies C1-C7.

## Correctness and compatibility

- collector-derived task and rollout identities are canonical; producer
mismatches are recorded and turn identities are normalized without
dropping the producer trajectory
- producer invocations remain authoritative on duplicate IDs,
observation-only invocations are appended, and model and tool records
are merged only by exact identifiers
- projection failures retain an explicit gap and captured request and
response payloads; successful projections remove the duplicate capture
payloads
- Simple Agent records the actual per-turn model input, cumulative
tool-step count, and an explicit gap when resolution is unavailable
- no existing fields are removed; response lifecycle status, provider
token details and totals, and cached-token aggregation are intentional
correctness changes; `ng_trajectory` is additive
- Simple Agent retains self-dispatch when observability is disabled or
`responses()` is overridden
- trajectory collection remains gated by observability and independent
of the token-ID capture correlation introduced by #2124
- captured request and response payloads are retained in persisted
`ng_trajectory`; LabBench's `multimodal_history_redacted` gap omits
those copies
- W&B rollout tables omit `ng_trajectory` and model-call request and
response payloads during collection and reverification
- payload projection has no separate size cap or trajectory-specific
opt-out; model-call capture remains opt-in

## NVBug alignment

- NVBug 6535274 is addressed with its preferred lossless option:
completed Responses calls preserve `response_status="completed"`;
`finish_reason` remains unset when the dialect does not provide one. The
`inference_provider` path now uses the shared Chat-to-Responses
converter, so the fix applies to the backend named in the report.
- This is the schema and reference-producer phase of NVBug 6555643 /
#1867. The capability matrix marks unsupported and path-dependent
producers as `X` or `O`; it is not a claim of full producer coverage.

## Follow-up pull requests

PRs #2115 and #2117-#2120 are stacked; later PRs include earlier stack
changes. Open PRs describe planned evidence and do not affect the
current capability matrix. #2153 provides Claude Code tool timing and
status; this PR joins tool results to those observations. No available
PR from #2115 through #2122 adds canonical C3 turns.

| PR | Producer or path | Evidence added | Remaining gap |
|---|---|---|---|
| #2115 | OpenClaw, PinchBench | Correlated model calls, conversations,
parallel tool timing, and sandbox observations | No C3 turns |
| #2116 | Claude Code | Superseded by #2153 | See #2153 and this PR |
| #2117 | Hermes | Parallel tool timing and agent observations | No C3
turns |
| #2118 | Pi | Correlated model calls, conversations, and parallel tool
timing | No C3 turns |
| #2119 | OpenCode | Rollout-level model calls, retained conversations,
and parallel tool timing | No exact per-invocation model-call ownership;
no C3 turns |
| #2120 | SWE OpenCode, OpenHands | OpenCode model-call correlation plus
retained conversations and sandbox observations for both paths |
OpenHands calls, tool timing, and C3 turns remain unavailable |
| #2121 | — | No PR exists | — |
| #2122 | Stirrup, GDPVal | GDPVal judge-call correlation; Stirrup and
Tau2 model-call correlation is already present | No agent turns or tool
observations |

## Validation

- 354 capture, conversion, trajectory, collector, reverification,
Fern-link, and inference-provider tests passed, plus 5 subtests
- 10 Simple Agent producer and dispatch tests passed
- in-process endpoint-to-record test passed: prefixed Responses request
→ `inference_provider` → capture middleware → `CaptureStore` →
`ModelCallRecord`
- producer-to-collector-to-JSON trajectory round trip passed with both
available and unavailable resolution status
- 110 vLLM model and Responses conversion tests passed
- 67 Claude Code observation tests and 4 LabBench redaction tests passed
- the trajectory patch merges cleanly with #2124; combined-stack
token-capture and trajectory tests passed
- Ruff check, Ruff format check, and `git diff --check` passed

Part of #1867.

---------

Signed-off-by: Michal Bien <mbien@nvidia.com>
)

## What

The OpenSandbox provider health-checks by default now:

- `connect()` and the reconnect in `_connect_after_create()` no longer
hardcode `skip_health_check=True`; both derive it from configuration,
whose default is `False`.
- Two shipped configs that turned the check off (`litmus_agent.yaml`,
and the example in the `mini_swe_agent_2` README) are flipped back on.

## Why

A sandbox id only proves the workload exists, not that its exec daemon
is listening. The server reports a sandbox ready once its pod is Running
with an IP, which happens before the daemon binds its port. A handle
returned without a health check defers that gap to the first real call,
where it surfaces as:

```
502 {"code":"GENERAL::UNKNOWN_ERROR","message":"Could not connect to the backend sandbox endpoint=..."}
```

Because `connect()` skipped the check unconditionally, the setting was
effectively opt-in rather than opt-out, and the two paths disagreed:
`create()` honoured `skip_health_check` while `connect()` ignored it.

`_verify_created_handle()` did not cover this either — it is a no-op
unless `probe.command` is configured.

This mirrors the reasoning already documented in the provider's own
reference config (`configs/opensandbox.yaml`), which sets
`skip_health_check: false`; this PR brings the code and the remaining
configs in line with it.

## Behaviour change

`connect()` now waits for the sandbox to answer instead of returning
immediately, so it can raise where it previously returned an unusable
handle. That is the intent: fail or wait at connect, rather than error
on the first command.

The opt-out is preserved via `skip_health_check` for callers that
deliberately want an unchecked handle.

Both updated configs also raise `create.timeout_s` above their
`ready_timeout_s`, following the guidance in the reference config: that
timeout bounds the whole create call, which now includes the readiness
wait, so leaving the two equal would turn the wait into a timeout.

## Testing

- Added `test_connect_health_checks_by_default` and
`test_connect_honours_skip_health_check_opt_out`.
- Updated two existing assertions that encoded the old hardcoded value.
- `tests/unit_tests/test_opensandbox_provider.py` and
`tests/unit_tests/test_sandbox.py`: all pass except
`test_resolve_provider_config_named_reference`, which fails on
`ModuleNotFoundError: No module named 'omegaconf'` and was confirmed
pre-existing by reproducing it with this change stashed.

Local runs used a minimal virtualenv: this repo pins a Python version
the local toolchain could not fetch, and one dependency does not build
on the newer interpreter available. Full-matrix verification is left to
CI.

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

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ch 2.1 config (#2296)

## What

Integrates the `nemo_gym.sandbox` API into the harbor agent
**non-intrusively**: a new `NemoGymSandboxEnvironment` (a Harbor
`BaseEnvironment`) executes Harbor trials inside sandboxes managed by
any NeMo Gym sandbox provider (opensandbox, docker, daytona, apptainer,
ecs_fargate). Selected purely via config — no changes to the
harbor_agent execution path.

- `custom_envs/nemo_gym_sandbox/environment.py` — AsyncSandbox-backed
backend: task.toml resources → `SandboxSpec`, tar-based
`upload_dir`/`download_dir` with per-file fallback, `bash -ic` exec
wrapping (parity with Harbor's docker/daytona backends),
always-terminate on `stop()`, opt-in
`allow_unenforced_internet_isolation`, `image_rewrites`.
- `configs/harbor_agent_opensandbox.yaml` — Terminal-Bench 2.1 (89
tasks) with stock Terminus-2 through the opensandbox provider in
server-proxy mode. Provider referenced via `sandbox_provider:
${sandbox}` (mini_swe_agent_2 style; include the shipped provider config
with a second `--config`). Sampling/harness match the Qwen3.6-27B model
card (temp 1.0 / top_p 0.95 / top_k 20, 3h agent timeout, 256K ctx / 80K
max output). 30 GiB sandbox ephemeral storage to avoid kubelet evictions
on disk-heavy tasks.
- `prepare_terminal_bench_2_1.py` + checked-in 89-row benchmark input
JSONL (pinned `harbor-framework/terminal-bench-2-1`).
- `harbor_ray_task_num_cpus` config knob — Harbor jobs are I/O-bound Ray
tasks; default 1 CPU/task caps concurrency at the driver CPU count, so a
fractional value (0.25) enables true full-89 concurrency on a 24-CPU
driver.
- fix: `extract_usage` sets `input_tokens_details.cache_write_tokens`
(required by openai ≥2.40; every live-model `/run` 500'd without it).
- 18 unit tests (fake registered provider; tar round-trip verified at
byte level).

## Result — Terminal-Bench 2.1, Qwen3.6-27B

Ran the full 89-task benchmark end-to-end: vLLM-served Qwen3.6-27B on 2
GPU replicas, gym/Ray driver, and sandboxes on a live OpenSandbox
deployment reached in server-proxy mode. Traces captured for all 89
tasks (rollouts + per-trial trajectory, asciinema recordings, verifier
artifacts).

| | Value |
|---|---|
| **This run** (TB 2.1, Harbor/Terminus-2, single run) | **pass@1 =
59.55%** (53/89) |
| **Model card** (TB 2.0, Harbor/Terminus-2, avg of 5 runs) | **59.3** |

**Reproduces the model-card Terminal-Bench number essentially exactly**
(within ~0.25 points). Caveats: TB 2.1 vs 2.0 (26 tasks fixed; same
count/harness), single run vs 5-run average, and sandbox resources set
to 4 vCPU / 16 GiB / 30 GiB.

Note: commits carry DCO sign-off; GPG signature to be added on amend
(non-interactive session).

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

## Update: connection hardening + 5-run measurement (in progress)

Follow-up commit hardens the opensandbox provider + harbor agent for
sandboxes reached across a load-balanced network path, based on failure
modes hit while running the benchmark end-to-end:

- **`connection.disable_connection_pooling`** (provider option, off by
default): a load balancer between client and OpenSandbox server can
silently reap idle pooled connections; the SDK then reuses a dead socket
and exec hangs (observed as dozens of CLOSE_WAIT sockets wedging tasks,
while fresh-connection requests completed in ~1s). With pooling disabled
every request opens a fresh connection — no reaped socket is ever
reused. Verified A/B: identical run with pooling on collapsed (8/89
tasks in 40 min, CLOSE_WAIT≈80); with the option on, 57/89 in 27 min
with CLOSE_WAIT=0.
- **`operations.background_exec`** (provider option, off by default):
runs each command as a background execution polled via short status/log
requests instead of holding one SSE stream open for the command's whole
duration — for load balancers that cap stream duration and would
otherwise drop the stream and hang the client. Costs extra round trips
per command. A window-matched A/B against the foreground SSE path scored
within run-to-run noise, so this is an availability option rather than a
quality one.
- **Hard exec wall-clock cap** (provider): backstop around
submit+poll+retries so a pathologically wedged exec raises
`TimeoutError` (which Terminus-2 records as a command timeout) instead
of hanging the task.
- ~~**`default_exec_timeout_s` 14400 → 300**~~ — *superseded*: that
reasoning accounted only for Terminus-2's tmux plumbing and missed
Harbor's **verifier**, which also calls `exec()` without `timeout_sec`.
The value is now **1800** in both the env default and the TB config —
see the gold-patch section below for the measurement.
- **`nemo_model_server_timeout_sec` 7200 → 2400**: lets a
silently-dropped model call self-heal within 40 min instead of blocking
a task for 2h.
- **`cpu_pin_enabled`** (opt-in, default false): pins every Harbor exec
(tmux session + verifier) to a random contiguous core block sized by the
task's cpu count, so `nproc` inside reports the real budget and
co-resident sandboxes spread across core blocks. Fail-open (no
`taskset`/small host → unpinned). Measured on a full TB 2.1 run under
card conditions: **no e2e-time or score delta vs baseline** (58.4% /
3h13m / 12.4% timeouts, all within the 5-run baseline spread) — task
wall time on this benchmark is dominated by model turns, not sandbox CPU
contention. Kept for CPU-bound workloads.

**Serving guidance for Qwen3.6 (hybrid GDN attention) on vLLM**, learned
the hard way:
- Use stock `vllm/vllm-openai` (v0.25.1 used here; multi-arch incl.
arm64).
- ~~**Disable prefix caching** (`--no-enable-prefix-caching`)~~ —
*superseded*: the engine-core freezes initially attributed to prefix
caching turned out to be the default (FlashInfer) GDN prefill kernel;
with `--gdn-prefill-backend triton`, prefix caching ON is stable
(validated over a further 5-run cohort, zero freezes) and scores better
— see the final card-parity section below.
- Warm each replica before an eval burst (a short + a long generation
crossing the ~8k boundary, plus a small concurrent batch) to pre-trigger
kernel JIT.

### 5-run pass@1 (final)

Two cohorts, differing only in the per-task agent timeout (the 4h budget
compensates the per-turn re-prefill slowdown from running without prefix
caching — see serving notes):

| Run | Agent timeout | pass@1 | agent-timeout rate |
|---|---|---|---|
| 1 | 3h | 57.30% (51/89) | 16.9% |
| 2 | 3h | 53.93% (48/89) | 20.2% |
| 3 | 4h | 62.92% (56/89) | 10.1% |
| 4 | 4h | 53.93% (48/89) | 11.2% |
| 5 | 4h | **59.55% (53/89)** | 10.1% |

- **3h cohort mean: 55.6** · **4h cohort mean: 58.8 ± 4.5** · all-5
mean: 57.5 ± 3.8
- References: earlier single clean run **59.55%**; model card **59.3**
(TB 2.0, 5-run average)
- The 4h cohort — whose timeout rate matches the card-harness conditions
(~10%) — lands on the model-card number within noise; run-to-run spread
of ±4-5 points on an 89-task benchmark is expected (1 task ≈ 1.1
points).

### Serving notes for Qwen3.6 (hybrid GDN) on vLLM

Hard-won operational guidance from ~30h of serving under full-benchmark
agentic load:

- **`--gdn-prefill-backend triton`** — with the default (FlashInfer) GDN
prefill kernel, the engine core froze roughly hourly under concurrent
long-context load: requests parked, prompt/generation counters static,
no errors, health endpoint green. Reproduced on two different vLLM
builds, with prefix caching on and off, on fresh and long-lived engines.
On the Triton/FLA kernel: **zero freezes across ~16h** of identical
load. (An upstream issue with the full signature is worth filing.)
- **Prefix caching: keep it ON once the Triton GDN kernel is in place.**
It was temporarily disabled while isolating the freeze (it's flagged
experimental for this hybrid-GDN family), but the final card-parity
cohort ran 5 freeze-free runs with caching enabled — and it's worth ~3–4
points on this workload: per-turn re-prefill dominates deep agentic
trajectories, so caching cuts turn latency, which cuts agent-timeout
rate (~11% vs ~13–17% cacheless) even on a shorter 3h budget.
- **`--compilation-config '{"cudagraph_mode": "PIECEWISE"}'`** —
attention ops run eagerly, outside graph replay.
- **Warm each replica before an eval burst** (one short + one long
generation crossing the ~8k boundary + a small concurrent batch) to
pre-trigger kernel JIT; and restart engines between long evals.
- **Protect long-running eval driver and sandbox pods from autoscaler
disruption** (a do-not-disrupt annotation or equivalent) — multi-hour
drivers are otherwise casualties of routine node consolidation, and a
mid-eval node roll takes in-flight sandboxes with it, surfacing as
opaque proxy 500s.
- **Sandbox resources 8 CPU / 16 GiB / 30 GiB** for TB 2.1's heavier
tasks.

### Follow-up: 4-replica scaling + SGLang A/B (5 runs each, 4h timeout,
8C/16G sandboxes)

| Serving stack | pass@1 (5 runs) | Mean ± std | Avg timeout rate |
Engine freezes |
|---|---|---|---|---|
| vLLM v0.25.1, `--gdn-prefill-backend triton` | 61.80, 55.06, 56.18,
56.18, 57.30 | **57.3 ± 2.6** | ~13% | 0 |
| SGLang v0.5.15.post1 (defaults + qwen3 reasoning parser) | 56.18,
56.18, 60.67, 51.69, 56.18 | **56.2 ± 3.2** | ~17% | 0 |

- Scaling 2→4 replicas doubled fast-phase throughput (~50% more
tokens/episode) but did not move pass@1 or the timeout rate: the
benchmark's deep tail is bounded by the per-task wall-clock budget, not
aggregate serving capacity.
- Both stacks ran ~20h each under full-benchmark load with zero engine
freezes — SGLang out of the box (after a readiness-probe adjustment: its
`/health` generates a token and can exceed a 1s probe timeout, flapping
healthy pods out of the Service; set probe `timeoutSeconds` ≥ 15), vLLM
with the Triton GDN kernel noted above. Scores are statistically
indistinguishable; vLLM sustained ~40% more tokens/episode at lower
timeout rates on this workload.

### Harness validation: gold-patch (oracle) run — 89/89

Ran all 89 TB 2.1 tasks with Harbor's `OracleAgent`, which uploads each
task's reference `solution/` and runs it instead of generating with a
model. This exercises the sandbox backend, `upload_dir`, exec and the
verifier end-to-end with the model removed from the equation, so the
expected score is a clean sweep.

**Result: 89/89 (reward 1.0 on every task), 22 min wall clock, zero
agent timeouts.**

Getting there surfaced one real bug in this integration, now fixed in
this PR:

**`default_exec_timeout_s` was silently truncating verification.**
Harbor's verifier calls `environment.exec()` **without** `timeout_sec`,
so this default — not the task's declared `[verifier] timeout_sec` — is
what bounds verification. The old values (300 in the environment, 600 in
the TB config) were justified by reasoning only about Terminus-2's short
tmux plumbing, which missed the verifier entirely. **87 of the 89 TB 2.1
tasks declare a verifier budget above 600s** (median 900, max 12000), so
verification was being killed mid-run and the task scored 0 instead of
failing loudly.

| exec cap | gold-patch score | what changed |
|---|---|---|
| 600s | 83/89 | `compile-compcert` killed at 602s; `query-optimize`
verifier killed at 604s → `RewardFileNotFoundError` |
| 1200s | 83/89 | `query-optimize` fixed; `compile-compcert` still
killed |
| **1800s** | **85/89** | both fixed |
| 12000s | 85/89 | no further gain |

Longest single command observed was 1384s, so **1800s is the smallest
value that costs nothing** — now the default in both the environment and
this config. Because this bug suppressed slow-verifying tasks,
previously reported pass@1 numbers are conservative.

The remaining 4 gold-patch failures were **not** harness issues and are
deliberately not fixed here — three were upstream drift (a dataset host
that refuses this network's egress; `RcppParallel 6.0.0` requiring
`cmake`, published 4 days before the run; `planarity 1.0.0` renaming
attributes `pyknotid` reads, published 4 weeks before) and one was macOS
`._*` sidecar files in our local dataset copy. Patching those reference
solutions would only change an oracle run — `solution/` is never
uploaded for a model agent — so it cannot affect a scored eval, and
fixing them for model runs would mean altering the task environment
rather than the solution. Worth noting for anyone reproducing: sandbox
resources were *not* implicated — the config's uniform 8 CPU / 16 GiB /
30 GiB is a strict superset of what all 89 task.toml files declare (max
4 CPU / 8 GiB / 10 GiB).

### Final: card-parity cohort (prefix cache ON + Triton GDN kernel, 3h
agent timeout — model-card conditions)

| Run | pass@1 | agent-timeout rate |
|---|---|---|
| 1 | 60.67% (54/89) | 10.1% |
| 2 | 61.80% (55/89) | 9.0% |
| 3 | 57.30% (51/89) | 13.5% |
| 4 | **66.29% (59/89)** | 11.2% |
| 5 | 59.55% (53/89) | 13.5% |
| **Mean** | **61.1 ± 3.3** (model card: 59.3) | 11.5% |

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

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Fixes #2217

## Problem

`gym eval run --no-serve --output results/mcqa_rollouts.jsonl` — the
quickstart command in the README — raises `FileNotFoundError` when
`results/` does not exist yet:

```
File "nemo_gym/rollout_collection.py", line 508, in run_from_config
    with config.materialized_jsonl_fpath.open("wb") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'results/mcqa_rollouts_materialized_inputs.jsonl'
```

The mkdir was already present in `run_from_config`, but it is called too
late. It ran at the top of the dispatch section, while the first thing
the run writes is the materialized inputs.

## Fix

Move `output_fpath.parent.mkdir(parents=True, exist_ok=True)` to the top
of `run_from_config`, above both the resume and fresh branches.

The issue suggested adding a mkdir at each of the four output paths. One
is enough: all four derive from `output_fpath` and keep its parent:
materialized inputs and aggregate metrics via `with_stem`, the failures
sidecar via `with_name`, rollouts is the path itself.

Resume semantics are unchanged: creating an empty directory does not
make `output_fpath.exists()` or `materialized_jsonl_fpath.exists()`
true, so resume eligibility is decided the same way as before.

## Why this only showed up on the PyPI install path

Two things were hiding it:

- Without `--no-serve`, `gym eval run` dispatches to
`e2e_rollout_collection`, which sets `output_dirpath = <output
parent>/preprocessed_datasets` and lets `TrainDataProcessor` create it,
incidentally creating the output parent. Only the `--no-serve` path goes
straight to `collect_rollouts`.
- The repo tracks a `results/.gitignore` placeholder, so `results/`
exists in a git clone. A PyPI install running from an arbitrary cwd has
no such directory.

## Scope

This was the only affected entry point. The others already create their
output parent first, so this brings rollout collection in line with the
existing convention:

- `gym eval reverify` — `rollout_reverification.py`, in
`_prepare_output_fpaths`
- `gym eval aggregate` — `rollout_collection.py`, in
`RolloutAggregationHelper.run_from_config`
- `gym dataset render` — `prompt.py`, in `materialize_prompts`
- `gym eval profile` — writes next to an input rollouts file that must
already exist

## Testing

Added `test_run_from_config_creates_missing_output_dir`, parametrized
over `resume_from_cache`, writing to a two-level-deep missing directory
and asserting all four artifacts land there. Both parametrizations fail
on `main` and pass with this change.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…oint override in /run (#2166)

## What does this PR do?

Adds optional `policy_base_url` / `policy_api_key` fields to the `/run`
request of both mini-SWE agents (`mini_swe_agent` and the sandbox-backed
`mini_swe_agent_2`), letting a caller point that one episode at its own
OpenAI-compatible policy endpoint. When the fields are absent, the
configured `model_server` is used — **default behavior is unchanged**.

On `mini_swe_agent_2`, the `ng-rollout` capture prefix is applied only
on the built-in model-server path: a caller supplying its own endpoint
owns routing (and any per-rollout correlation) for it.

## Motivation

We are integrating NeMo Gym as an environment ecosystem for RL training
in [miles](https://github.com/radixark/miles) (an open-source RL
training framework), driving the mini-SWE agents with one `POST /run`
per training episode.

For lossless on-policy training (token-in/token-out), the trainer routes
every policy call of an episode through a per-episode recording proxy:
each episode gets its **own** OpenAI-compatible URL (e.g.
`http://trainer:30000/sessions/<session_id>/v1`), and the proxy captures
token ids / logprobs / loss masks for exactly that episode. This pattern
isn't specific to our trainer — any external RL framework that records
or routes rollouts per episode needs a per-request policy endpoint.

Today `run()` builds the endpoint from the statically configured model
server, fixed for the lifetime of the server process, so this
integration currently requires carrying a fork of `app.py`. Everything
downstream is already per-request capable — the swegym/sandbox runners
accept `base_url` / `api_key` per invocation and inject them into
`model_kwargs` — so this PR only wires the request fields through.

The field names mirror the existing global-config keys
(`policy_base_url`, `policy_api_key`). If this pattern proves useful
beyond the mini-SWE agents, the fields could later be promoted to
`BaseRunRequest`; a complementary general mechanism (per-rollout
outbound routing at the model-server layer, building on the existing
`ng-rollout` correlation prefix) could serve agents that call the policy
through `server_client` — happy to open a separate issue to discuss
that.

## Testing

- `ruff check` / `ruff format --check` pass on the touched files.
- No behavior change when the new fields are unset (they default to
`None`; the existing paths are taken).
- Happy to add unit tests for the override path if you can point me at
the preferred test location for responses-API agents.

---------

Signed-off-by: Tao Lin <tao.lin@radixark.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`model-recipes/nemotron-3-nano.mdx` tells users to provision ~110GB in
the prerequisites and again when choosing a workspace, then checks for
200GB two lines later.

Both figures now say 200GB, matching the existing success check. The
prerequisite also states where the space goes, using numbers the page
already verifies elsewhere: the container is ~15GB (step 1.4) and the
model is ~59GB (step 1.6), with the rest covering data, the Hugging Face
cache, and checkpoints and logs that accumulate across runs.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Co-authored-by: Anwith Kiran <anwithk@nvidia.com>
`contribute/agent-skills.mdx` links to the Add a Benchmark page with a
full `docs.nvidia.com` URL that carries no version slug. The URL
resolves (200), so nothing is broken, but it drops readers on the
default version regardless of which version they were reading.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…n output (#2305)

Three small fixes (defects D, F, H): `gym list models --json` now
includes a `name` key like every other component type (`model` kept, so
`jq '.[].model'`still works); model inspection shows a usage example
like benchmarks and environment already did, and `gym env packages` only
prints its header in the default view so `--json` pipes cleanly.

Agents still show no usage example - their names aren't valid `--agent`
values, so there's nothing runnable to suggest until #1583 lands a real
agent selector.

---------

Signed-off-by: Ewa Dobrowolska <edobrowolska@nvidia.com>
Co-authored-by: kajalj22 <kajalj@nvidia.com>
## Summary
- Rewrite [Key
Terminology](https://docs.nvidia.com/nemo/gym/main/about/concepts/key-terminology/)
with Gym-aligned definitions (Environment, Agent, Rollout, Benchmark,
etc.)
- Add an Overview (run flow, Agent/Environment composition, Model
server) plus a concept → Gym component map
- Cross-link related how-tos (Data, Agent/Model Server, Verifiers,
Evaluate, Training, Sandboxes)

Fixes #1500

## Test plan
- [x] `make docs-check` (0 errors)
- [x] `python3 tests/unit_tests/test_fern_docs_links.py`
- [x] Confirm Fern docs preview comment on the PR
- [x] Skim Overview + glossary sections for clarity on the preview URL

---------

Signed-off-by: Seph Mard <smard@nvidia.com>
Signed-off-by: Seph Mard <seph.mard@gmail.com>
Co-authored-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
…#2353)

## Summary

- **vllm** 0.20.0 → 0.24.0 (addresses security vulnerabilities; also
updates flashinfer-python 0.6.8.post1 → 0.6.12)
- **GitPython** ≥3.1.50 → ≥3.1.57 (addresses security vulnerability;
lock resolves to 3.1.58)
- **pyarrow** already at 25.0.0 in lock (existing `>=23.0.1` constraint
sufficient; no change needed)
- **diskcache** added to `exclude-dependencies` (no fix available; no
longer pulled in by vllm 0.24.0 — blocked explicitly to prevent
re-introduction)

vllm 0.24.0 does not import `NamespaceTool` from
`openai.types.responses`, so the existing `openai<=2.7.2` constraint is
preserved and no openai SDK bump is required.

## Test plan

- [x] CI passes (lint, unit tests, pre-commit hooks, server suite,
build)
- [x] `uv lock` resolves cleanly with no conflicts

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

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
## Summary
- Document the Resources Server protocol landscape, including the
seed/verify and reset/step base episode protocols and existing
seed/verify extensions.
- Clarify how Agent Server `run()` and `responses()` behavior maps to
Resources Server lifecycle, verification, and transition APIs.
- Explain direct HTTP, MCP, Gymnasium step-action, bespoke transition,
and agent-embedded tool interactions.
- Provide API contracts, implementation examples, state management,
cleanup, YAML configuration, and matching built-in Agent Servers.
- Add the guide to the Build Environments navigation.

Closes #1363

## Test plan
- [x] Run `npm run check` from `fern/`

---------

Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
## Summary
- Closes [#1360](#1360)
- Document execution-based verification for code, SQL, tool state,
formal proofs, and sandboxed workloads.
- Cover concurrency, timeouts, isolation, cleanup, and failure handling.
- Add the guide to the Verification Patterns navigation.

## Test plan
- [x] Run `cd fern && npm run check`

Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
The Workplace Assistant docs quote four different figures for the same
environment: 26 tools / 5 databases, 27 tools / 5 databases, 27 tools /
6 databases, and 27 tools split as five toolkits plus a company
directory.

`get_tools` in `resources_servers/workplace_assistant/utils.py` is the
source of truth. It always registers the company directory lookup, then
adds one toolkit per requested name. Running it with the five toolkits
`app.py` seeds each session with:

```
functions: 27   schemas: 27
containers: analytics, calendar, company_directory, customer_relationship_manager, email, project_management
per-toolkit: email 6, analytics 6, calendar 5, project_management 5, crm 4, company_directory 1
```

So: 27 tools across five mutable databases plus a read-only company
directory. `is_correct` (utils.py:166) compares final state for the five
databases and not the company directory, which only exposes
`find_email_address`.

## Changes

- `evaluation/environment-list.mdx`: 26 tools to 27 tools
- `generating-training-data.mdx`: "27 tools across 6 databases" to five
databases plus a company directory lookup
- `about-workplace-assistant.mdx`: the 27 tools are the five databases
plus the company directory, not 27 tools spread across five databases
- `environments/workplace_assistant/README.md` and
`resources_servers/workplace_assistant/README.md`: 26 tools to 27 tools

`mcp-resources-server.mdx` already matched the code and is unchanged.

## Test

`resources_servers/workplace_assistant/tests/test_docs_counts.py`
derives the count from `get_tools` and asserts every page and README
that quotes it. Adding or removing a tool now fails a test instead of
leaving the docs stale.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
#2332)

## Summary

Adds two lightweight verifiers used by the blended Nemotron Super Omni
RL dataset.

`string_match` — exact/fuzzy answer comparison with boxed/latex
extraction. Covers 14,988 of the 60,299 rows in that blend.
`gui_coordinate` — 2D coordinate containment. Covers 16 rows.

Both subclass `BaseVerifyResponse` and follow the existing simple-agent
contract, so they need no special handling in the rollout path.

Carrying them here removes the need for downstream launchers to copy
verifier directories into the Gym checkout at submit time.

---------

Signed-off-by: DanialTaheri <smohsenitahe@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…coordinate (#2377)

`gym env test` counts every directory under `resources_servers/` etc. as
a candidate module, but only tests the ones containing a `README.md`
(`nemo_gym/cli/env.py:717`), then asserts the two counts match. #2332
added `string_match` and `gui_coordinate` without READMEs, so main fails
with `Mismatch on the number of total modules found (144) and the number
of actual modules tested (142)`.

This adds a README to each, describing the extraction/scoring behavior,
the input JSONL fields, and how to run them.

Both servers' tests now run: 10 passed for `string_match`, 12 for
`gui_coordinate`. Data validation preconditions hold for both (5
examples, 5 rollouts, `"Number of examples": 5`).

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Partially addresses #2216, and fixes the quickstart report behind it:
`gym env start` prints "All 3 / 3 servers ready" against a
`policy_base_url` that nothing is serving, and `gym eval run` then sits
at `Collecting rollouts: 0%`.

Gym reports ready because `wait_for_spinup` in the env CLI polls only
Gym's own servers. The Gym-side model server is a proxy that answers as
soon as it starts, whether or not an inference server is behind it, so
the endpoint the
run depends on is never checked. The existing troubleshooting note for
this path blames a bad API key, which fails differently.

This collects the model endpoints from the resolved config, waits for
each to answer, and stops startup if one never does.

Reading the endpoints has to handle every shape the model servers
declare:
- `base_url` is `Union[str, List[str]]` on vllm_model and both local
vLLM servers, where the list spreads load across replicas, so a
string-only reader would do nothing at all for a multi-replica training
run.
- The local servers default to an empty list and are filled in after Gym
launches vLLM, so an empty list means "not yet" and is skipped, as is an
unset value.
- The config key travels with each URL, because the endpoint that fails
may be a judge or user model and a message naming `policy_base_url`
would then be wrong.

Each probe result is sorted by whether waiting could change it. A
hostname that does not resolve will not begin resolving because we
waited, so it is reported once and not retried; this is what happens to
a config left at a
placeholder default, which costs one lookup instead of the whole
timeout.

A refused connection may be a server still coming up, so it is waited
on.

Any HTTP answer counts as listening, including 401 and 404, and so does
a completed TLS handshake against a certificate this machine does not
trust.

`requests.exceptions.SSLError` subclasses `ConnectionError`, so deciding
on the parent class would reject an HTTPS endpoint with an internal
certificate authority even though its handshake proved something was
there.

On failure, the spawned servers are shut down before anything is raised.
They hold ports and have neither a process group nor an atexit handler,
and every caller reaches its own `shutdown()` only after `start()`
returns.

The failure is a `ConfigError` that the CLI entrypoints translate into
an exit. `RunHelper` is imported and
driven directly by NeMo-RL, so the caller decides what an unreachable
endpoint means.

Probing uses the synchronous `requests` library, matching
`poll_for_status` in server utils, because this runs in the CLI process
before any event loop exists.



## The change in flow

Before:

```mermaid
flowchart TD
    S["gym env start"] --> P["start mcqa, agent, policy_model"]
    P --> W["wait_for_spinup()"]
    W --> Q{"do Gym's own servers answer on /?"}
    Q -->|yes| OK["All 3 / 3 servers ready"]
    OK --> U["gym eval run"]
    U --> M["policy_model calls its configured endpoint"]
    M --> X["nothing listening<br>Collecting rollouts: 0% indefinitely"]
```

After:

```mermaid
flowchart TD
    S["gym env start"] --> P["start mcqa, agent, policy_model"]
    P --> W["wait_for_spinup()"]
    W --> N["collect model endpoints from the resolved config<br>string and list base_url, skipping unset and empty"]
    N --> PR{"probe each endpoint"}
    PR -->|"any HTTP answer, or a completed TLS handshake"| OK["All 3 / 3 servers ready"]
    PR -->|"name does not resolve"| REP["report once, do not wait<br>waiting cannot create a DNS record"]
    REP --> OK
    PR -->|"connection refused"| WAIT["wait, a server may still be starting"]
    WAIT -->|answers in time| OK
    WAIT -->|"still refusing at the timeout"| SD["shut down the spawned servers"]
    SD --> CE["raise ConfigError naming each URL<br>and the config key it came from"]
```

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
## Summary

- Snapshots `fern/versions/latest/` → `fern/versions/v0.5.0/` (101
files) to freeze the docs at the 0.5.0 GA state
- Creates `fern/versions/v0.5.0.yml` with nav config pointing to the
frozen pages
- Registers `v0.5.0` as `stable` in `fern/docs.yml` (between Main and
0.4.0)
- Adds v0.5.0 release notes accordion to `release-notes.mdx` in both
`latest/` and the snapshot
- Drops pre-release tag: `PRE_RELEASE = "rc0"` → `""` in
`nemo_gym/package_info.py` (version is now `0.5.0`)

## Test plan

- [ ] Verify `docs.nvidia.com/nemo/gym` shows `0.5.0` in the version
picker as stable
- [ ] Verify the v0.5.0 docs snapshot renders correctly at `/v0.5.0/`
- [ ] Verify release notes page shows v0.5.0 accordion open by default,
v0.4.0 collapsed
- [ ] Verify `nemo_gym.__version__` returns `"0.5.0"` (no `rc0` suffix)
- [ ] Cherry-pick to `r0.5.0` after merging to main

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

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
## Summary

Fixes the `test_internal_pages_are_linked_by_path_not_by_absolute_url`
test failure introduced by #2389.

The v0.5.0 release notes sandboxing section contained a
`docs.nvidia.com/nemo/gym/...` absolute link. The test requires all
internal docs links in `latest/` to use relative paths (to keep readers
within the version they're reading). Replaced with a relative path
`/nemo-gym/nemo_gym/sandbox/providers` in both `latest/` and the
`v0.5.0/` snapshot.

## Test plan

- [ ] `test_internal_pages_are_linked_by_path_not_by_absolute_url`
passes
- [ ] Cherry-pick to `r0.5.0`

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

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
## Summary
- Remove the NeMo Evaluator row from the Evaluation & Training table on
About → Ecosystem

## Test plan
- [x] `make docs-check`
- [x] Confirm Fern preview on the PR
- [x] Verify Ecosystem page no longer lists NeMo Evaluator under
Evaluation & Training

Signed-off-by: Seph Mard <smard@nvidia.com>
ananthsub and others added 22 commits August 25, 2026 11:23
Documents training on rollouts from an external agent harness that
drives its own model calls.

## Summary
- Explains run-level infrastructure enablement, static per-agent
selection, `all_agents`, and the explicit `/training-token-capture`
request path.
- Distinguishes training capture from neutral rollout correlation and
evaluation observability.
- Documents async sink/source contracts, frozen snapshots, durable
incomplete state, frozen retirement tombstones, and conditional
retirement after downstream durability.
- Shows how a training framework supplies paired transport-backed
endpoints without adding framework dependencies to Gym.
- Covers worker-local installation limits, startup validation,
selected-agent finalization, reconstruction masking, rollout identity,
and first-run metrics.
- Provides the caller-owned `finalize_rollout_token_capture` and
`retire_rollout_token_capture` sequence.

The page is linked from the training tutorials index. Parent resolution
and prefix supply are documented in #2349.

---------

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
<!-- Thanks for contributing to NeMo Gym! Please fill out the sections
below. -->

## What does this PR do?

Declares `failure_reason` field on `BaseVerifyResponse` to standardize a
way to report why a returned reward may not reflect the policy quality.
`failure_reason` is meant to be human-readable; see #2750 for the whole
design document.

## Checklist

- [ ] I have read the [contributing
guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup).
- [ ] 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).
- [ ] Pre-commit checks pass locally (`pre-commit run --all-files`) (so
CI lint/format/copyright pass).
- [ ] All commits have DCO sign-off (`git commit -s`) (so the DCO check
passes).

---------

Signed-off-by: Teodor-Dumitru Ene <teodord.ene@gmail.com>
## Summary

- emit standardized `ng_agent_observations` for the legacy SWE
OpenCode/OpenHands paths and the decoupled `opencode_sandboxed_agent` +
SWE-bench path
- correlate OpenCode calls through rollout-prefixed Gym Model Server
routes when rollout observability or token capture is active
- compose OpenCode invocation/tool evidence with separate agent- and
verifier-sandbox observations
- preserve direct response behavior, grading outputs, retained
artifacts, and `subagent_trajectories`

## What changed

### Decoupled OpenCode / SWE-bench

- parse OpenCode SQLite sessions into invocations, parent relationships,
cumulative model-visible conversations, tool timing/outcomes, compaction
events, and exact response IDs
- keep the sandboxed agent independently installable by copying the
minimal parser closure locally; it no longer imports the standalone
`opencode_agent`, and a fresh-process test enforces that boundary
- isolate OpenCode data per observed run, download and parse the
database before teardown, and remove the local scratch database
afterward
- record the connected agent sandbox's real provider/ID and compose
verifier-sandbox lifecycle evidence returned by
`resources_servers/swebench`
- emit observations only when a capture-derived rollout ID exists;
direct `/v1/responses` behavior remains unchanged

### Legacy SWE harness

- OpenCode records retained session invocations, parent relationships,
exact response IDs, and the latest cumulative conversation;
rollout-prefixed model calls provide capture and token accounting
- OpenHands records its available cumulative root conversation and
sandbox evidence, while explicitly reporting that exact model-call
correlation is unavailable with the pinned fork
- legacy Apptainer records leave `sandbox_id` unset and report
`sandbox_identity_unavailable` because the runner exposes no real
sandbox handle

Observation construction fails open. Missing or malformed evidence
becomes an explicit gap; provider sentinel values are not exposed as
process exit codes, and unavailable resource or lifecycle measurements
remain unset rather than inferred.

## Capability coverage

| Producer | C1 | C2 | C3 | C4 | C5 | C6 | C7 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `opencode_sandboxed_agent` | V | V | X | V | V | V | V |
| `swe_agents` / OpenCode | V | V | X | V | X | X | V |
| `swe_agents` / OpenHands | X | X | X | O | X | X | V |

The capability matrix documents these evidence boundaries. Legacy
artifacts do not provide standardized semantic turns, authoritative
per-tool timing, or independent parallel-tool timing.

## Validation

- 34 combined standalone and sandboxed OpenCode tests, including
fresh-process import isolation
- focused SWE-bench resource-server and legacy SWE-agent tests across
disabled, observability-only, token-only, and combined capture states
- 114 shared rollout-observability, correlation, and collection
regressions rerun after the final rebase
- SQLite artifact -> parser -> decoupled `/run` composition test
covering invocation, tool, agent-sandbox, verifier-sandbox, and cleanup
primitives
- Ruff, formatting, Python compilation, `git diff --check`, and scoped
pre-commit hooks

A real artifact-compatibility smoke test used Docker Server 29.6.2 on
Linux/aarch64 and the exact
`swebench/sweb.eval.x86_64.astropy_1776_astropy-12907` image under
x86_64 emulation. OpenCode 1.17.11 was installed only inside the
temporary container and ran a real gpt-5.5-backed session whose bash
tool executed `printf opencode-observability-smoke`; the actual tool
result persisted and `opencode export` succeeded.

The WAL-mode database contained 1 session, 3 messages, and 7 parts.
After closing/exporting, only `opencode.db` was copied and parsed by the
final sandbox-local parser, producing 1 completed invocation, 1 tool
call, 0 compactions, and only the expected
`model_call_ownership_unavailable` gap. The temporary container was
stopped and auto-removed; nothing was installed on the host.

## Limitations

- the live smoke used the direct NVIDIA gateway rather than Gym's
rollout-prefixed model proxy, so it validates the real OpenCode artifact
schema/parser but not model-call ownership or capture joining
- OpenCode's final prose stream did not terminate after the tool result
and was gracefully interrupted
- the full decoupled `/run` + verifier flow was not run live because
`DockerProvider` cannot reconnect across the resource-server and agent
processes; that path requires OpenSandbox
- the SQLite parser is intentionally duplicated to keep the two agent
servers dependency-isolated and must remain synchronized

## Related rollout-observability work

- #2114 shared observation and correlation contract
- #2153 Claude Code producer
- #2115 OpenClaw producer
- #2117 Hermes producer
- #2118 Pi producer
- #2119 standalone OpenCode producer

---------

Signed-off-by: Michal Bien <mbien@nvidia.com>
## Summary

Adds multi-node vLLM service support to the Slurm orchestration layer
(`gym eval submit`), building on top of the existing single-node
multi-instance support (separate PR:
`onur/multi-instance-vllm-service`). A `vllm` service can now span
multiple physical Slurm nodes, either for a single replica's
tensor/pipeline-parallel footprint, or for multi-node data-parallel
replicas.

What's NOT included in this PR is that multi-node TP with multiple
instance which is a rare case. So, in order to use the features in this
PR, the model has to fit into a single node.

## What's new

- **`ray` distributed backend** — a new `distributed_backend` option
(alongside the existing single-node `mp` backend) using vLLM's Ray core
executor (`--distributed-executor-backend ray`, *not* the `ray.serve`
library) to span a service across nodes.
- **Automatic backend selection by node count** — if
`compute.node_pools` total more than one node, `distributed_backend` is
automatically forced to `ray`, overriding anything set/defaulted at the
service level. No need to write `distributed_backend: {type: ray}`
yourself.
- **Multi-node data-parallel validation** — `number_of_instances` must
divide evenly across the node count (each node hosts an equal share of
replicas).
- **Sbatch script generation** (`slurm_script.py`):
- injects a Ray head-node-IP prelude (`scontrol show hostnames`,
`HEAD_NODE_IP`/`RAY_HEAD_NODE_IP`) when any service uses the `ray`
backend,
- single-instance multi-node: wraps the vLLM command in `ray
symmetric-run` (falling back to manual `ray start --head`/`--block` for
older Ray), spanning TP/PP across nodes,
- multi-instance multi-node: uses vLLM's native multi-node data-parallel
pattern — head node serves the OpenAI API, worker nodes run `--headless`
with a `--data-parallel-start-rank` offset — no Ray involved for that
path,
- adds `--nodes=`/`--ntasks=` to each service's `srun` step on
multi-node compute (`--nodes=1 --ntasks=1` for the driver).

## New example config

- `examples/slurm_vllm_ray_multi_node.yaml` — 2-node compute, 8 replicas
(TP2) spanning both nodes, backend auto-selected as `ray` from node
count alone.

---------

Signed-off-by: Tomasz Grzegorzek <tgrzegorzek@nvidia.com>
Signed-off-by: prokotg <19536019+prokotg@users.noreply.github.com>
Signed-off-by: Onur Yilmaz <oyilmaz@nvidia.com>
Signed-off-by: Onur Yilmaz <35306097+oyilmaz-nvidia@users.noreply.github.com>
Co-authored-by: Tomasz Grzegorzek <tgrzegorzek@nvidia.com>
Co-authored-by: prokotg <19536019+prokotg@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Judge models are commonly hosted independently of other jobs. This
allows for their reuse.

If a judge model must be rehosted, we cannot allow this to bring down
the entire training cluster.

Commonly, training infrastructure have job duration limits. If a judge
model meets such a limit in the middle of a training run, it will change
addresses, and it needs to communicate this to the rest of the Gym
setup.

This PR adds two optional arguments:
- `max_connection_retries` prevents infinite retries (which cause hangs)
- `endpoint_file` allows the judge model to communicate its address via
a local filesystem; this flag comes with associated helper configs.

---------

Signed-off-by: Teodor-Dumitru Ene <teodord.ene@gmail.com>
## Summary
- `tests/e2e/run_inference_provider_e2e.sh` now runs against a dedicated
`tests/e2e/inference_provider_smoke.jsonl` prompt (unambiguous weather
request) instead of the shared `example_single_tool_call` dataset, and
pins `--temperature 0` for determinism. Mirrors the existing
`tests/e2e/gpu_smoke.jsonl` pattern used by `gpu_e2e_test.sh`.
- `provider_e2e_tests` in `cicd-main.yml` now uploads
`rollouts.jsonl`/`gym.log` as a build artifact on every run (mirrors
`gpu_e2e_tests`), for debugging future failures.

## Test plan
- [ ] `fireworks-e2e` CI job passes on this PR

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Summary
- Bump Pillow from `~=11.0.0` to `==12.3.0` in
`responses_api_agents/osworld_agent/requirements.txt`, matching the pin
already used across every other agent/resource server in the repo.
- `osworld==0.1.0` transitively pins `pillow>=11.0.0,<11.1.dev0`, which
made the plain version bump unresolvable. Added an `overrides.txt` entry
to bypass that stale pin, following the same pattern used in
`stirrup_agent`.

## Test plan
- [x] `uv pip install -r requirements.txt --override overrides.txt`
resolves cleanly and installs `pillow==12.3.0` (verified locally)
- [x] `uv lock --check` passes (unaffected — this server sits outside
the uv workspace)
- [ ] CI passes

---------

Signed-off-by: Kajal Jain <kajalj@nvidia.com>
moves gsm8k w calculator, hotpotqa w search, bixbench and bbh to
environments

---------

Signed-off-by: Christian Munley <cmunley@nvidia.com>
<!-- Thanks for contributing to NeMo Gym! Please fill out the sections
below. -->

## What does this PR do?

<!-- Briefly describe the change and the motivation. Link any related
issue, e.g. "Closes #123". -->

## Checklist

- [ ] I have read the [contributing
guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup).
- [ ] 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).
- [ ] Pre-commit checks pass locally (`pre-commit run --all-files`) (so
CI lint/format/copyright pass).
- [ ] All commits have DCO sign-off (`git commit -s`) (so the DCO check
passes).

Signed-off-by: Christian Munley <cmunley@nvidia.com>
Signed-off-by: Christian Munley <cmunley@nvidia.com>
…ext search (#2722)

## Summary

Adds a local backend for `edgar_search` in `finance_sec_search`,
answering
full-text queries over SEC filings from a read-only SQLite FTS5 index.

The tool name, description and parameter schema match the hosted
equivalent,
so an existing dataset runs against it unmodified.

Opt-in and additive:

- `local_edgar_index_path` enables it; unset (the default) and
`edgar_search`
  reports itself unavailable.
- `convert_questions.py` still defaults to `sec_filing_search`, so
  `data/example.jsonl` is unchanged.

## What's included

- `local_edgar_search.py` — query translation, filtering, paging,
ranking.
- Optional metadata sidecar built by
`scripts/build_local_edgar_metadata.py`,
validated at startup against a fingerprint of its source index. On a
~570k
document index it takes common queries from tens of seconds to under
one.
- `convert_questions.py --search-tool` to choose the filing-search tool.
- `docs/local_edgar_index.md` — the index schema, the column formats it
depends on, and how to obtain an index. No builder ships here; an index
is
a self-contained file plus its sidecar and can be copied between
machines.

## Training

Used for GRPO training runs against a held-out financial analysis
benchmark
(not included here), 3 seeds, mean ± std across seeds. Baseline is the
untrained model with the same tool set, so the comparison is like for
like.

|  | Baseline | Step 6 | Step 16 | Step 18 | Step 21 |
|---|---|---|---|---|---|
| Balanced accuracy | 19.58 ± 2.26 | 21.38 ± 1.53 | 24.44 ± 4.50 | 25.45
± 2.08 | 26.23 ± 1.11 |
| Accuracy | 24.50 ± 1.50 | 25.67 ± 2.02 | 29.17 ± 3.51 | 30.83 ± 2.52 |
30.83 ± 1.26 |
| Turns / question | 19.4 ± 0.4 | 19.5 ± 1.2 | 18.1 ± 0.4 | 17.0 ± 0.5 |
17.0 ± 0.5 |
| Output tokens / question | 21293 ± 203 | 20742 ± 756 | 13214 ± 429 |
11973 ± 295 | 11358 ± 132 |
| `edgar_search` calls | 3.34 ± 0.10 | 3.33 ± 0.29 | 2.99 ± 0.29 | 3.02
± 0.22 | 2.75 ± 0.08 |
| `parse_html_page` calls | 3.30 ± 0.22 | 3.24 ± 0.23 | 2.85 ± 0.06 |
2.70 ± 0.11 | 2.66 ± 0.07 |
| `retrieve_information` calls | 4.61 ± 0.28 | 4.57 ± 0.28 | 4.53 ± 0.07
| 4.33 ± 0.14 | 4.25 ± 0.09 |
| `web_search` calls | 7.20 ± 0.14 | 7.42 ± 0.59 | 6.76 ± 0.26 | 6.00 ±
0.17 | 6.39 ± 0.29 |

Accuracy rises from 24.50 to 30.83 while output tokens per question fall
by
47%, with fewer calls to every tool.

## Testing

`ng_test +entrypoint=resources_servers/finance_sec_search` — 84 passed.

New coverage: query translation, request normalization, date/form/CIK
filtering, paging, ranking order, filter-only browsing, sidecar build
and
fingerprint rejection, latency metrics, and the server route with and
without an index configured.

---------

Signed-off-by: Ushnish De <ude@nvidia.com>
Co-authored-by: Christian Munley <cmunley@nvidia.com>
## Summary

- replace the benchmark-local, user-wide cleanup helper with a
standalone OpenSandbox cleanup CLI that requires an exact run and user
scope
- attach the Slurm run and user attribution to every sandbox created by
the evaluation
- submit one CPU-only `afterany` cleanup job immediately after the
evaluation job, giving cleanup its own allocation and time budget after
success, failure, timeout, or cancellation
- paginate the full sandbox inventory, delete matches concurrently
through one bounded connection pool, and repeat the sweep to handle
teardown races
- keep credentials in the existing connection config; the dependent job
receives only its path and the exact cleanup scope

## Test plan

- [x] focused cleanup and launcher tests: 37 passed locally, cleanup
module at 100% coverage
- [x] Bash 5 launcher behavior checks for workload failure, TERM
cancellation, and server-first exit
- [x] scoped pre-commit hooks, shell syntax check, and `git diff
--check`
- [x] live Slurm dependency probe: plain parent cancellation released
the `afterany` CPU job and inherited GPU requests were overridden
- [x] live cancellation E2E with real attributed rollouts plus
controlled isolation fixtures: matching target deleted with HTTP 2xx,
same-user wrong-run decoy preserved, cleanup completed successfully, and
the final exact-scope audit found zero survivors
- [x] fixture teardown verified no test fixtures remained

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a connection (#2558)

## What

`pty.exec(detach=True)` runs a command without holding a connection
while it works: the command starts in a PTY session, the WebSocket is
dropped, and the session is briefly re-attached every `poll_interval_s`
to check for completion. A long-running command occupies a connection
for milliseconds per poll instead of its whole runtime.

## Why

At eval scale, session-mode PTY holds one WebSocket per rollout for the
rollout's lifetime — thousands of standing connections held for hours.
Detached exec replaces that with a handful of short-lived polls (~200 ms
each), a ~100x reduction in standing connections, and structurally
avoids the failure modes long-lived sockets are exposed to
(load-balancer idle timeouts on quiet sessions, server deploys
interrupting streams).

## How

- `OpenSandboxPtySession.detach()` / `reattach()`: built on the existing
resume machinery (`takeover=1`, `since=<bytes received>`). execd
sessions run fine with no client attached — the socket is a view, not
the session's lifeline. A detached session is not `closed`, so provider
pruning leaves it alone; `close()` still releases and ends it.
- `pty.exec(..., detach=True, poll_interval_s=...)`: same marker
discipline as session-mode exec, plus file capture inside the sandbox
(`>cap.out 2>cap.err`) because the server retains only ~1 MiB of
terminal output across a detach. Output is `cat`-collected on
completion, so stdout/stderr come back separated in both pty and pipe
modes. A fast command that finishes within the first quiet window never
detaches at all.
- Without `session` a private session is opened (never registered as the
default-shell session) and closed afterwards. An explicitly passed
session is detached while the command works and comes back attached and
reusable.

## Testing

- Unit: wire-level detach/reattach (no DELETE on detach,
`since`/`takeover` on re-dial, prune safety, close-after-detach) and
facade-level detached exec (poll cycle, fast path, private-session
lifecycle, timeout parity with `exec()`). 107 tests passing in
`test_opensandbox_pty.py` + `test_sandbox.py`.
- E2E SWE-bench eval with the agent command running detached, with
connection-count telemetry: in progress, will post results here.

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

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: bxyu-nvidia <bxyu@nvidia.com>
<!-- Thanks for contributing to NeMo Gym! Please fill out the sections
below. -->

## What does this PR do?

1. Fix DeepSWE from @nv-mengxiwu : resources_servers/deepswe/app.py
2. Fix OpenCode performance regression:
responses_api_agents/opencode_sandboxed_agent/app.py

## Checklist

- [ ] I have read the [contributing
guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup).
- [ ] 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).
- [ ] Pre-commit checks pass locally (`pre-commit run --all-files`) (so
CI lint/format/copyright pass).
- [ ] All commits have DCO sign-off (`git commit -s`) (so the DCO check
passes).

Signed-off-by: Brian Yu <bxyu@nvidia.com>
## Summary

- Add the required `type: "message"` discriminator to the tutorial's
assistant output fixture.
- Mark the message as completed so the copied verifier test matches the
current Responses schema.

## Test plan

- [x] Replayed the tutorial scaffold, app, config, data, and tests
- [x] Ran `gym env test --resources-server my_weather_tool`.
- [x] Ran `cd fern && npm run check`.

Closes #2695.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…me (#2786)

## Problem

In `benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh` the reported
model name is derived from the serve path — one variable feeds both:

```bash
vllm serve "$MODEL"
++policy_model_name=$MODEL
```

Under the current coupling, the only way to give a run a meaningful name
is to rename the mount, and that is a trap: anything else addressing
that mount by path (`--chat-template /checkpoint/...`,
`--reasoning-parser-plugin /checkpoint/...`) silently points at a path
that is no longer mounted, and vLLM fails to load it. We hit exactly
that.

## Change

`MODEL_NAME` names the model independently of where it is mounted:

- vLLM serves under it (`--served-model-name`), so the endpoint answers
to it.
- The eval sends it (`++policy_model_name`), so the two cannot disagree.

This lets the caller keep the mount fixed and still say which checkpoint
a run used.

Tested on a run

---------

Signed-off-by: plaszkiewicz <plaszkiewicz@nvidia.com>
`save_model_call_using_vllm_tokenize_endpoint` reads
`len(response["tokens"])`, which only exists on older vLLM builds;
mainstream >=0.19.1 returns `{"count": N, "max_model_len": ...}` and
omits the token list unless token ids are requested, so the pre-call
context-reset estimation dies with KeyError: 'tokens' on every sample.
Prefer the explicit count, fall back to the token list, and fail loudly
when neither is present.

<!-- Thanks for contributing to NeMo Gym! Please fill out the sections
below. -->

## What does this PR do?

<!-- Briefly describe the change and the motivation. Link any related
issue, e.g. "Closes #123". -->

## Checklist

- [x] I have read the [contributing
guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup).
- [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] Pre-commit checks pass locally (`pre-commit run --all-files`) (so
CI lint/format/copyright pass).
- [x] All commits have DCO sign-off (`git commit -s`) (so the DCO check
passes).

---------

Signed-off-by: MJ Mikulski <mmikulski@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## What does this PR do?

Implements the [Rollout Quality Verification in NeMo Gym
RFC](https://rfc.frontier-evals.nvidia.com/r/gym-rollout-verification)
as an automatic, report-only health pass after evaluation.

The implementation adds:

- a typed check registry (`CheckSpec`) and structured evidence
(`Finding`)
- a focused internal package: `health/types.py` for contracts and
`health/checks.py` for canonical evidence handling and checks, while
`rollout_health.py` remains the public orchestration workflow
- runner-derived `healthy`, `unhealthy`, and `unobserved` verdicts;
checks never emit verdicts
- a three-phase runner that indexes JSONL byte offsets, evaluates
rollouts in a process pool, then deterministically reduces per-rollout
digests into run/task reports; no Ray dependency
- `quality_summary.json` and `rollout_verdicts.jsonl`, keyed by
`_ng_task_index` and `_ng_rollout_index`
- `gym eval health-check <run-dir>` for standalone verification
- automatic health checks after `gym eval run` and `gym eval aggregate`
- `--no-health-check`, worker-count controls, and explicit per-check
exclusions for historical-corpus analysis
- a short terminal summary without changing collection output, scores,
or aggregate metrics

Refs #2136.

## Canonical observability boundary

Health reads exactly one observability representation: the final
`TrajectoryRecord` persisted under each rollout record's `ng_trajectory`
key. It never discovers or reads model-call sidecars,
`ng_model_call_capture`, agent-specific transcripts, invocation
conversations, or legacy `response.output` shapes.

Gym's existing observability stack may use those sources internally
during collection. Before the rollout is written, it normalizes them
into:

- `ng_trajectory.turns` for agent turns
- `ng_trajectory.model_calls` for model-call evidence
- `ng_trajectory.gaps` for evidence that could not be collected or
joined exactly

Health then applies one conservative rule: missing canonical evidence
makes only the dependent checks unobserved, while an explicit
contradiction in the canonical trajectory produces a finding. For
example, `turns_unavailable` makes turn checks unobserved; incomplete
model-call evidence makes binding-dependent checks unobserved; and
explicit unmatched, ambiguous, or conflicting model-call references
produce `trajectory_capture_mismatch` findings. Unreferenced calls do
not fail because they may belong to a judge, user simulator, or another
auxiliary model.

## Check registry

The RFC's check families are represented as single-responsibility IDs.
The correspondence family is split because missing token fields,
reference contradictions, failed calls, and total-token disagreements
have different subjects and missing-input behavior. `record_unreadable`
is an explicit extension for artifact parse failures.
`check_execution_error` separately reports an unexpected exception in
check code, so it is not misattributed to a successfully parsed record
or semantic check. `rollout_duplicate_identity` preserves and flags
physical records that claim the same logical rollout identity while
counting that identity only once for task-level repeat checks.

| Check | Evaluation scope | Finding subject |
|---|---|---|
| `check_execution_error` | rollout | check execution |
| `record_unreadable` | rollout | rollout record |
| `rollout_duplicate_identity` | rollout | rollout |
| `rollout_missing_agent_turns` | rollout | rollout |
| `agent_turn_hollow` | rollout | agent turn |
| `model_call_zero_completion_tokens` | rollout | model call |
| `model_call_missing_token_counts` | rollout | model call |
| `trajectory_capture_mismatch` | rollout | trajectory/model-call
binding |
| `model_call_failed` | rollout | model call |
| `rollout_token_count_mismatch` | rollout | rollout |
| `model_call_runaway_generation` | rollout | model call |
| `task_consistently_unhealthy` | task | task |
| `task_no_successful_model_calls` | task | task |

## Verdict semantics

`healthy` is intentionally a strong claim:

1. any finding makes the rollout `unhealthy`;
2. otherwise, any enabled check lacking required evidence makes it
`unobserved`;
3. only a fully evaluated rollout with no findings is `healthy`.

Turn checks require canonical `TrajectoryTurn` evidence. Policy-model
checks use only canonical model calls explicitly referenced by canonical
turns. A turn without a model-call reference is not itself a failure;
binding-dependent checks become unobserved when no usable references
exist.

## CLI

```bash
# Check an existing run directory
gym eval health-check <run-dir>

# Name a nonstandard rollout file explicitly
gym eval health-check <run-dir> --rollouts-file evaluator_rollouts.jsonl

# Limit worker processes or exclude known checks for one analysis
gym eval health-check <run-dir> --workers 4 \
  --ignore-checks model_call_missing_token_counts,rollout_token_count_mismatch

# Opt out of the automatic post-run/post-aggregate pass
gym eval run ... --no-health-check
gym eval aggregate ... --no-health-check
```

Automatic paths also expose `--health-check-workers` and
`--health-check-ignore`. Ignored checks remain registered but do not
execute or affect findings, unobserved states, verdicts, or task flags.
The summary records which checks were ignored and their coverage counts.

## Output guarantees

- Reports are deterministic and sorted by task/repeat identity.
- Every non-empty input line receives a verdict row, including malformed
records.
- Missing canonical observability degrades dependent checks to
unobserved rather than false failure.
- Duplicate task/repeat identities remain visible at run scope but are
collapsed before task-level repeat reduction.
- If a process pool is unavailable, the same work completes serially
with a warning.
- Health checks do not mutate rollout collection artifacts or aggregate
metrics.

## Validation

- `pytest tests/unit_tests/test_rollout_health.py
--cov=nemo_gym.rollout_health --cov=nemo_gym.health --cov-fail-under=96
-q` — 42 passed; 98.43% coverage
- focused CLI, collection, observability, and model-capture tests — 378
passed
- `uv build` — sdist and wheel pass; the wheel contains
`nemo_gym.health`
- scoped pre-commit hooks pass for every changed file
- full local unit suite — 2,779 passed, 41 skipped; three existing
failures outside the touched paths remain on macOS (port binding, Linux
`/proc`, and an AIME metadata fixture)

Synthetic tests cover every registered semantic finding, each missing
canonical evidence state, canonical bindings and gap translation,
malformed records, duplicate identities, task reduction, serial
fallback, CLI/config validation, and ignored checks. A golden test
proves that collection artifacts and aggregate metrics are
byte-identical with automatic health checks enabled and disabled.

Historical VPR corpora used during development predate persisted
`ng_trajectory`; under this final single-source contract they correctly
report the trajectory-dependent checks as unobserved instead of being
heuristically retrofitted. Positive real-corpus validation now requires
runs produced by the current observability stack.

## Checklist

- [x] I have read the [contributing
guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup).
- [x] The change is focused; unrelated drive-by edits are tracked
separately.
- [x] Tests added or updated and pass locally, subject to the documented
pre-existing full-suite exceptions.
- [x] Pre-commit checks pass for the changed files.
- [x] All commits have DCO sign-off (`git commit -s`).

---------

Signed-off-by: Giulio Lovisotto <glovisotto@nvidia.com>
Co-authored-by: Marta Stepniewska-Dziubinska <marta-sd@users.noreply.github.com>
…2765)

## Summary

Background-command **status polls** are sub-second idempotent GETs, but
they inherit the general `request_timeout_s` budget (tuned for long
submits/creates, often minutes). Against a sandbox whose pod has become
unreachable (a blackholed IP that swallows SYNs), each poll then hangs
for a full TCP connect timeout before failing — retries × minutes per
dead sandbox before the typed unreachable error fires, tying up a
rollout slot the whole time.

### feat: dedicated short timeout for background status polls

- New `operations.status_poll_timeout_s: float | None = 10.0`:
per-request budget for background-command **status polls only**. A
healthy poll answers in milliseconds and even the slow legitimate path
(endpoint re-resolution plus a connect retry) is a few seconds. `None`
restores the previous behavior; submits keep their `timeout_s + 60`
headroom and the logs fetch keeps the shared budget.
- A per-call timeout surfaces as builtin `TimeoutError`, which the
default retry classifier deliberately treats as terminal (a long timeout
has already burned its budget — and `TimeoutError` subclasses `OSError`,
which would otherwise be retried). With a 10s budget that would make a
single slow poll rollout-fatal, so `_await_sdk_operation` gains a
pluggable `is_retryable` predicate (default unchanged for all other
callers) and the status-poll path passes one that retries timeouts —
safe because a status poll is an idempotent GET, unlike a submit where a
timeout must stay terminal.
- With the retry budget, an unresponsive sandbox is detected in ~1
minute instead of ~25+.

### fix: stop relabeling inner timeouts as the exec hard cap

Pre-existing bug the short poll budget makes frequent: since Python 3.11
`asyncio.TimeoutError` IS builtin `TimeoutError`, so the
`wait_for`-based hard-cap wrapper around exec dispatch also caught any
`TimeoutError` raised *inside* the dispatch (e.g. a status poll
exhausting its budget and retries) and relabeled that minutes-scale
failure as `exceeded hard cap of <hours>s; the command wedged` —
factually wrong, corrupting failure taxonomy. Now uses `asyncio.timeout`
and attaches the wedged message only when the cap itself expired
(`Timeout.expired()`); inner timeouts propagate with their original
message.

## Note on the branch's original scope

This PR originally implemented duplicate-submission protection for
background commands (adopting an already-scheduled execution after a
lost submit response). Per-request tracking deployed at production scale
(>115k requests across multiple 1024-way-parallel runs, with
body-hash-level duplicate detection) observed **zero** duplicate
submissions in practice, so that machinery was withdrawn to keep the
provider simple; this PR now ships only the status-poll timeout work
above.

## Tests

51 passed on the provider suite: parametrized per-operation timeout
routing (poll gets the short budget; submit and logs keep theirs),
`None` fallback, poll-timeout retryability, config validation,
inner-timeout propagation with original message (fails against the old
`wait_for` code), and a genuinely wedged dispatch still receiving the
hard-cap label.

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

---------

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…2791)

## Problem

`benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh` locates the
run-scoped cleanup job's script under `submit_dir=$(pwd -P)`, which
assumes the launcher is invoked from a Gym checkout. A caller that
launches from a scheduler run directory has no such tree there, so the
submission fails outright:

```
sbatch: error: Unable to open file <rundir>/nemo_gym/sandbox/providers/opensandbox/cleanup_sandboxes.py
Failed to submit cleanup job for batch job 6540264; the batch job is still active
```

No cleanup job is scheduled and sandboxes are never reaped. This is how
NVIDIA's eval-factory benchmarking runner drives the launcher: it stages
a copy of the script into a per-run directory and `cd`s there, because
that is where `slurm-logs/` and `results/` live.

## Changes

1. `NEMO_GYM_REPO_ROOT` names the checkout, alongside the launcher's
other declared inputs at the top of the file. It defaults to the working
directory, so a launch from a checkout is unchanged.
2. The connection comes from `OPENSANDBOX_DOMAIN` and
`OPENSANDBOX_API_KEY` — the same two variables the run itself resolves
`opensandbox.yaml` from. The launcher no longer looks for an `env.yaml`
and no longer forwards `--connection-config`; the flag stays on the
script for a manual run.
3. A failed cleanup submission no longer strands the main job. It
printed no job ID and exited 1, so a caller read a live 4-node job as a
failed submission — and resubmitted it. It now prints `Submitted batch
job <id>`, warns that its sandboxes need reaping by hand, and exits 0.

Dropping the `env.yaml` route is deliberate: that file is merged over
every config a run names, so a connection read from it can silently
override the evaluated config, and no hash covers it.

## Testing

`pytest tests/unit_tests/test_opensandbox_cleanup.py` — 40 passed, 3
skipped. `ruff check` and `ruff format --check` clean on both changed
files.

## Supersedes

Replaces #2788, rebased onto current `main` and squashed. Same change,
opened from a branch whose commits carry a sign-off.

---------

Signed-off-by: Piotr Laszkiewicz <plaszkiewicz@nvidia.com>
Signed-off-by: plaszkiewicz <plaszkiewicz@nvidia.com>
Co-authored-by: Grzegorz Chlebus <gchlebus@nvidia.com>
…g to Exa (+other cfg changes to align w NEL) (#2779)

## What

Replaces the `bash -c` subprocess behind the BrowseComp harness's
`bash_command` tool with `pocketshell` — an in-process,
workspace-confined read-only shell, vendored into the server directory
so the harness stays self-contained.

Also aligns `benchmarks/browsecomp/config.yaml` with how the benchmark
is actually run.

## Why

The previous guard was a command-**name** deny/allow list plus `ulimit
-f 0`. It never inspected path arguments, so `cat /etc/passwd` passed
it, and its own comment described it as "NOT a security boundary".
`pocketshell` cannot execute anything it does not implement, and
confines every path to the sample workspace, so the protection is
structural rather than advisory.

## Compatibility

No accuracy-affecting change is intended:

- the legacy deny/allow guard is kept **in front**, unchanged, so
anything it used to reject still returns the byte-identical `[blocked:
...]` string;
- the output shape (`stdout` / `--- stderr ---` / `[exit_code=N]`) is
preserved;
- grammar and command coverage were sized against 1.65M real
agent-written `bash_command` calls; the unsupported syntax (command
substitution, heredocs, process substitution, backgrounding) is exactly
what the old guard already rejected.

## Config changes

`benchmarks/browsecomp/config.yaml` now reflects the run recipe:

- `search_provider: exa` + `max_results: 10`
- `progress: true` — the durable progress board becomes the default
- `max_run_retries: 3 -> 1` (per-sample retries)
- judge moves off `policy_model` onto a dedicated `judge_model`, so a
weak policy can no longer grade itself
- agent `model_server`: `policy_model_no_interleaved_reasoning` ->
`policy_model`; interleaved reasoning is now always on
- drops `keep_rounds` (identical to the field default) and
`context_reset_pct` (dead whenever `context_reset_tokens > 0`) — both
inert
- drops the unreferenced `Qwen3-235B-A22B-Instruct-2507-FP8` node

API keys use `${oc.select:...,null}` so an unset key resolves to null
instead of failing interpolation.

## Testing

215 tests pass (73 harness + 68 pocketshell + 74 agent) against current
`main`. `ruff check` and `ruff format --check` clean. All commits DCO
signed.

---------

Signed-off-by: Ritu Gala <rgala@nvidia.com>
Co-authored-by: bxyu-nvidia <bxyu@nvidia.com>
## Summary

This PR adds model-specific serving configurations for Qwen3.5-122B-A10B
and Inkling Small, configurable decode routing and evaluation
concurrency, resumable rollouts, fail-fast multi-node serving, and
benchmark-level failure containment.

## Shared infrastructure and benchmark reliability

- **Configurable decode routing:** allow tuned runs to override the
default `cache_aware` decode-routing policy while preserving that
default for callers that do not set an override.
  ```bash
  VLLM_DECODE_POLICY=power_of_two \
  NUM_DECODE_NODES=4 \
  bash benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh ...
  ```
- **Opt-in evaluation concurrency:** pass the number of parallel Gym
samples only when explicitly configured. This enables throughput tuning
without changing existing model runs by default.
  ```bash
  NUM_SAMPLES_IN_PARALLEL=64 \
  bash benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh ...
  ```
- **Resumable evaluations:** use a stable output path and reuse
completed rollouts after a Slurm requeue or manual resubmission with the
same compatible experiment configuration. Note: reuses compatible
completed rollouts when a command is restarted with the same experiment
name. It does not itself request a Slurm requeue.
  ```bash
  RESUME_EVAL_ON_REQUEUE=1 \
  EXPERIMENT_NAME="<stable-experiment-name>" \
  bash benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh ...
  ```
- **Fail-fast multi-node serving:** add Slurm's `--kill-on-bad-exit=1`
so one failed vLLM worker terminates the server step and propagates
through the launcher's existing lifecycle monitoring instead of leaving
a partial deployment running. The Gym readiness timeout remains
configurable.
  ```bash
  bash benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh \
    --config <config.yaml> \
    ++model_endpoint_readiness_timeout_seconds=1200
  ```
- **Tau2 setup and rollout containment:** serialize Tau2 data
initialization; validate model-emitted tool arguments; retry malformed
JSON up to five generations; and, if a rollout still fails, emit a
structured zero-reward failure record instead of an HTTP 500 that aborts
the full suite.
- **Container prerequisite:** install the locked profiling dependency
required by `gym eval prepare` and verify it while building the
evaluation image.
  ```bash
  INPUT_CONTAINER="<base.sqsh>" \
  OUTPUT_CONTAINER="<with-gym.sqsh>" \
  MOUNTS="<mounts>" \
  GYM_CONFIG="<config.yaml>" \
  sbatch benchmarks/nemotron_3.5_super/build_eval_container.sh
  ```
- **OpenCode export paths:** strip the sandbox `pwd` newline before
building the export path, preventing valid `/testbed/export.json`
artifacts from being requested with a trailing newline.

## Qwen3.5-122B-A10B

### Model-specific changes

- Configure Qwen's tool-call and reasoning parsers, multimodal encoder
parallelism, and TP4/EP4 execution.
- Split NIXL KV-cache transfer into prefill-producer and decode-consumer
settings.
- Use one DP1-per-node TP4 prefill server and four DP1-per-node TP4/EP4
decode servers (P1/D4), matching main's launcher topology.
- Use fixed-scale FP8 KV cache, CUDA graphs, synchronous scheduling, and
a 512-sequence engine limit.
- Enable two-token multi-token prediction (MTP, speculative decoding
from the model's prediction heads) on both serving roles when
`QWEN_ENABLE_MTP=1`; matching settings preserve compatible transferred
KV-cache layouts.
- **Implementation:**
`benchmarks/nemotron_3.5_super/vllm_configs/qwen3.5-122b-a10b.sh`.

### How to run

```bash
MODEL=/lustre/fsw/portfolios/llmservice/users/igitman/hf_models/Qwen3.5-122B-A10B \
VLLM_CONFIG=benchmarks/nemotron_3.5_super/vllm_configs/qwen3.5-122b-a10b.sh \
QWEN_ENABLE_MTP=1 \
RESUME_EVAL_ON_REQUEUE=1 \
EXPERIMENT_NAME=super3.5-e2e/qwen3.5-122b-a10b-p1d4-dp1-c64-p2-ep4x4-seqs512-cg256-fp8kv-scale1-sync-nopc-mtp2 \
NUM_SAMPLES_IN_PARALLEL=64 \
NUM_PREFILL_NODES=1 \
NUM_DECODE_NODES=4 \
VLLM_DECODE_POLICY=power_of_two \
EXPORT_TO_CSV=1 \
SBATCH_TIME=<time-limit> \
SBATCH_ACCOUNT=<slurm-account> \
SBATCH_PARTITION=<slurm-partition> \
SBATCH_QOS=normal \
SBATCH_GRES=gpu:4 \
CONTAINER=$(pwd)/results/vllm/vllm-openai:v0.25.1___tomer_with_gym.sqsh \
MOUNTS=/lustre:/lustre,$(pwd):/opt/Gym \
bash benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh \
  --config benchmarks/nemotron_3.5_super/eval_container_config.yaml \
  ++model_endpoint_readiness_timeout_seconds=1200
```

### Validation

The full 11,213-rollout suite completed on HSG GB200 with P1/D4,
concurrency 64, TP4/EP4, MTP2, and zero failed rollouts.

| Serving metric | Average | Minimum | Maximum |
|---|---:|---:|---:|
| Output tokens/s/request | 149.6 | 127.2 | 177.4 |
| Waiting requests | 0 | 0 | 0 |
| GPU KV-cache usage | 10.7% | 3.0% | 14.5% |
| Aggregate output throughput | 8,701 tok/s | 6,400 tok/s | 9,492 tok/s
|

| Benchmark | Score |
|---|---:|
| Tau3-Banking | 14.2% |
| Tau3-Average | 60.1% |
| SciCode — whole problem | 8.1% |
| SciCode — subtask | 38.2% |
| AA-LCR | 65.9% |
| AA-Omniscience (OmniIndex) | 25.4% |
| GPQA Diamond | 85.9% |

## Inkling Small

### Model-specific changes

- Add Inkling's V2 runner, tokenizer mode, FlashAttention CuTe DSL
cache, tool-call parser, reasoning parser, and FlashInfer-autotune
setting from the upstream recipe while intentionally serving the local
BF16 checkpoint.
- Use TP4/EP4, prefix caching, chunked prefill, synchronous scheduling,
and full-decode CUDA graphs.
- Split NIXL transfer roles and use two DP1-per-node prefill servers
plus four DP1-per-node decode servers (P2/D4), matching main's launcher
topology. The second prefill node removes the sustained queue observed
with P1/D4; a larger token budget alone did not remove it.
- Give each prefill engine a 16,384-token scheduling budget and each
decode replica an 8,192-token budget.
- Make MTP draft width configurable and apply it symmetrically to
prefill and decode. MTP2 was selected because its approximately 63%
acceptance avoided the throughput collapse observed when drafting all
eight tokens.
- **Implementation:**
`benchmarks/nemotron_3.5_super/vllm_configs/inkling_small.sh`.

### Container

Inkling required a separate recipe-compatible image layered with Gym and
`vllm-router`. The validated image is
`results/vllm/vllm-openai:nightly-inkling-small-20260815_with_gym.sqsh`,
built from vLLM `0.27.2rc1.dev77+gac7509e2b`. The recipe's compatibility
floor is vLLM 0.26; this records the exact newer build tested.

### How to run

```bash
MODEL=/lustre/fsw/portfolios/llmservice/users/lvega/models/Inkling-Small \
VLLM_CONFIG=benchmarks/nemotron_3.5_super/vllm_configs/inkling_small.sh \
INKLING_ENABLE_MTP=1 \
INKLING_MTP_NUM_SPECULATIVE_TOKENS=2 \
RESUME_EVAL_ON_REQUEUE=1 \
EXPERIMENT_NAME=super3.5-e2e/inkling-small-bf16-p2d4-dp1-c64-p2-ep4x4-seqs256-cg256-sync-pc-mtp2-pbt16384 \
NUM_SAMPLES_IN_PARALLEL=64 \
NUM_PREFILL_NODES=2 \
NUM_DECODE_NODES=4 \
VLLM_DECODE_POLICY=power_of_two \
EXPORT_TO_CSV=1 \
SBATCH_ACCOUNT=<slurm-account> \
SBATCH_PARTITION=<slurm-partition> \
SBATCH_QOS=normal \
SBATCH_TIME=<time-limit> \
SBATCH_GRES=gpu:4 \
CONTAINER=$(pwd)/results/vllm/vllm-openai:nightly-inkling-small-20260815_with_gym.sqsh \
MOUNTS=/lustre:/lustre,$(pwd):/opt/Gym \
bash benchmarks/nemotron_3.5_super/sbatch_external_vllm.sh \
  --config benchmarks/nemotron_3.5_super/eval_container_config.yaml \
  ++model_endpoint_readiness_timeout_seconds=1200
```

Note: The full suite also requires the externally staged, gitignored
`benchmarks/scicode/data/test_data.h5` fixture.

### Validation

The validation run completed all 11,213 rows on HSG GB200 in 2:30:13. It
produced 11,212 normal rows and one structured failure caused by an
external Omniscience judge HTTP 500, not by Inkling or vLLM.

| Serving metric | Result |
|---|---:|
| Output tokens/s/request | 107.06 mean; 96.77 p5; 89.74 minimum;
826/827 loaded windows at least 90 |
| Derived local decode queue | 0 in all 3,432 samples |
| Literal decode waiting | NIXL-transfer deferrals only; per-replica
maxima 2, 3, 4, and 6 |
| Prefill waiting | Nonzero in 14/1,718 samples; brief maxima 8 and 5;
no sustained buildup |
| GPU KV-cache usage | 3.74-3.97% mean; 8.04-9.45% p95; 14.2-18.9%
maximum |

| Benchmark | Score |
|---|---:|
| Tau3-Banking | 11.3% |
| Tau3-Average | 59.5% |
| SciCode — whole problem | 16.9% |
| SciCode — subtask | 44.5% |
| AA-LCR | 66.9% |
| AA-Omniscience (OmniIndex) | 33.8% |
| GPQA Diamond | 87.9% |

- **Note on CRITPT batching:** Main commit `54cee4f1` (merge commit
`ef1dd91b`) enabled CRITPT in the Super suite. CRITPT's public scorer
waits for 70 distinct problems before scoring a batch. The suite
interleaves 5x70-item batches. At concurrency 64 (our tuned setting for
optimal output tokens/s/request) every Gym slot can become occupied by a
rollout waiting for an incomplete batch and block eval progress.

To clear CRITPT, you can cancel and resubmit with
`RESUME_EVAL_ON_REQUEUE=1` and `NUM_SAMPLES_IN_PARALLEL=350`. This will
lower output tokens/s/request but provide enough slots for all 350
CRITPT rollouts to complete their batches. Once all 350 CRITPT rows have
been scored and written to `resumable.jsonl`, either cancel and resume
at concurrency 64 or allow the run to finish at concurrency 350.

---------

Signed-off-by: Frankie Siino <fsiino@nvidia.com>
Signed-off-by: Brian Yu <bxyu@nvidia.com>
Co-authored-by: Brian Yu <bxyu@nvidia.com>
@agronskiy
agronskiy requested a review from a team as a code owner September 1, 2026 11:42
@copy-pr-bot

copy-pr-bot Bot commented Sep 1, 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.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Merges `main` at b793621 ("Super 3.5 vllm
model tuning (Qwen3.5-122B-A10B, Inkling-Small)", #2599) rather than the
current `main` tip.

WHY THIS TARGET

b793621 is the commit immediately before 65129dd ("chore(deps): pin
openai to 2.44.0", #2456). That PR added

    class NeMoGymChatCompletionCreateParamsNonStreaming(BaseModel):
        model_config = ConfigDict(extra="forbid")

which makes the proxy reject every agentic chat-completions request that
carries `chat_template_kwargs` — the field agentic GDPVal runs use to drive
thinking budgets on self-hosted models. Each such request comes back as

    422 Unprocessable Entity
    {"detail":[{"type":"extra_forbidden","loc":["body","chat_template_kwargs"],
     "msg":"Extra inputs are not permitted","input":{"thinking":true}}]}

so the rollout fails before it reaches the model. Merging `main` past
65129dd would take that regression into the GDPVal branch and break the
production runs this branch exists to serve.

b793621 was chosen because it is the last commit that is regression-free
while still giving the branch everything else it needs:

  * `requires-python = ">=3.13.14"`, so the py313 runtime container stays
    correct;
  * `extra="forbid"` appears exactly once in nemo_gym/openai_utils.py, on
    `NeMoGymResponseCreateParamsNonStreaming` (the pre-existing 2025
    Responses-class validation) and NOT on the ChatCompletion class;
  * the rollout-observability work (bc521f7) is an ancestor.

CONFLICTS AND RESOLUTIONS

Eight files conflicted; all GDPVal files auto-merged. Every conflict was
resolved to keep both sides' behaviour rather than pick a winner.

  * nemo_gym/cli/env.py, cli/setup_command.py and their tests:
    `get_venv_path` and `resolve_server_venv_path` are the same function
    under two names (byte-identical bodies). Canonicalised on devel's name
    and docstring, kept main's `Path(...)` normalisation of
    `root_venv_path`, renamed main's call sites and the `monkeypatch.setattr`
    in test_cli.py, and dropped the now-unused `ROOT_DIR` import.

  * nemo_gym/openai_utils.py: took main's content-part types — its
    `video_url` part accepts `Union[str, Dict[str, Any]]` where devel's was
    dict-only, and it adds `NeMoGymChatCompletionContentPartFileParam` —
    while keeping devel's comment recording the 422 that motivated adding a
    video part at all.

  * responses_api_models/local_vllm_model/setup.py: both sides pin
    vllm==0.24.0 at this merge target, so the resolution is main's file
    (which additionally pins flashinfer-python==0.6.12) plus devel's note
    that 0.24.0 is the first release with MiniMax-M3 support.

  * nemo_gym/rollout_reverification.py: plain union of both import lists
    from nemo_gym.rollout_collection; dropped devel's
    `NG_TERMINAL_KEY as NG_TERMINAL_KEY` re-export idiom, nothing needs it.

  * nemo_gym/rollout_collection.py: kept both sides' features — main's
    exporters (`upload_rollouts`/`export_rollouts`/`get_exporters`,
    replacing the W&B-specific path) and token-capture retirement, devel's
    dispatch budget, drain margin and `DispatchLatencyTracker`,
    `kill_shaped` no-persist rows, `_validate_dispatch_concurrency` and
    `ordered_tasks`. Three resolutions here are semantic rather than
    textual:
      - main's `from time import time` SHADOWS the `time` module that
        devel's `time.monotonic()` calls need, so the module import was kept
        and main's two bare `time()` calls rewritten as `time.time()`;
        taking main's side verbatim compiles and then fails at runtime;
      - the persistence branch takes main's flat
        `if no_persist / elif failure_class is not None / else` chain with
        its token-capture retirement, dropping devel's `result_strs.append`
        (it fed the removed W&B table) and the stale "not the W&B table
        either" wording in the kill_shaped comment;
      - devel's `ordered_tasks = [asyncio.ensure_future(...)]` scheduling
        was kept, because main still passes bare coroutines to
        `as_completed`, which makes `dispatch_longest_first` a no-op, and
        the shared code after the conflict references `ordered_tasks`.

  * tests/unit_tests/test_rollout_collection.py: import union, and devel's
    version of the fresh-run cleanup test, which is a superset of main's and
    additionally asserts the aggregate-metrics file is cleared. The fresh-run
    cleanup in `run_from_config` now unlinks the failures sidecar and the
    aggregate-metrics file so a re-run cannot inherit retry attempts or
    published metrics from an older run at the same output path.

VERIFICATION

`ruff check` and `ruff format --check` with the CI-pinned ruff 0.9.9 are
clean across nemo_gym, tests, responses_api_models and resources_servers.

resources_servers/gdpval/tests: 386 passed, 11 skipped — the
`reference_missing` path (#2796) and `strict_comparison_trials` (#2807)
are both intact.

tests/unit_tests against an `origin/main` baseline built in a scratch
worktree on the same venv: 79 failures on the baseline, 101 here. The 44
new failures are all in test_openai_utils.py (43) plus
test_responses_api_model_streaming.py::test_prunes_nested_extra_fields, and
are an environment artefact, not a merge defect: b793621 predates #2456
and still declares `openai<=2.7.2`, while the venv has openai 2.44.0
installed. Those tests enumerate the *installed* SDK's item tags
(`shell_call`, `apply_patch_call`, `compaction`, `tool_search_call`, ...)
and require a Gym union member for each; the pre-2.44 schemas in this tree
have none.

Re-running the same suite with an openai inside the declared pin (2.7.2
shadowed onto PYTHONPATH, venv untouched) gives 1 failed / 2976 passed —
the single failure, test_opensandbox_cleanup.py::test_script_help_runs_by_
direct_path, is also present in the origin/main baseline. So the new-failure
set against baseline is EMPTY once the SDK matches the pin, and no failure
touches rollout collection, reverification, the CLI or GDPVal.

Signed-off-by: Alex Gronskiy <agronskiy@nvidia.com>
@agronskiy
agronskiy force-pushed the agronskiy/gdpval-devel-with-main branch from 8cba113 to 21b0408 Compare September 1, 2026 16:07
@github-actions github-actions Bot added the sla:triage-overdue Review assignment is over the one-business-day SLA label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sla:triage-overdue Review assignment is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.