Skip to content

feat(benchmarks): explicit agent pin on benchmark datasets; scaffold generates the decoupled layout - #2724

Merged
adil-a merged 6 commits into
mainfrom
data-prep/benchmarks-scaffold
Aug 28, 2026
Merged

adil-a merged 6 commits into
mainfrom
data-prep/benchmarks-scaffold

Conversation

@adil-a

@adil-a adil-a commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  1. A benchmark's agent is inferred from config position. Whichever agent block declares the benchmark dataset becomes the benchmark's agent. Nothing enforces that this inference is well-defined, and a benchmark's score depends on the harness — the resolution rules deserve to be explicit and validated.
  2. The inference breaks on real configs. bigcodebench's manifest pulls in the base environment config, so its merged config has two agents on the same resources server; which one "declared the dataset" is an accident of merge order.
  3. gym env init still generates the old layout. New environments are born with datasets: under the agent block — every scaffold run creates more configs the migration has to fix later.

Change

1. Benchmark agent resolution becomes an explicit, validated rule set (with an optional agent: key as the disambiguator for genuinely ambiguous configs — unambiguous configs, which is all of them today, need no pin).

# Before — the agent is whichever block holds the dataset (nothing says so):
aime24_math_with_judge_simple_agent:
  responses_api_agents:
    simple_agent:
      datasets:
      - name: aime24
        type: benchmark
        jsonl_fpath: benchmarks/aime24/data/aime24_benchmark.jsonl
        num_repeats: 32

# After — the dataset lives on the resources server; the harness resolves from the unique
# agent → resources-server edge (an `agent:` key on the dataset can override when ambiguous):
aime24_math_with_judge:
  resources_servers:
    math_with_judge:
      datasets:
      - name: aime24
        type: benchmark
        jsonl_fpath: benchmarks/aime24/data/aime24_benchmark.jsonl
        num_repeats: 32

How a benchmark finds its agent now, first hit wins:

Rule Covers
1. The dataset's agent: key the escape hatch for ambiguous configs (none exist in-repo today)
2. The agent block that declares the dataset every manifest that declares the dataset inside an agent block — unchanged behavior. Note this rule is unambiguous when it applies — the bug was never single-declarer inference, it was the ambiguous case below.
3. The dataset sits on a resources server with no pin, and exactly one agent's config contains resources_server: {name: <that server>} → that agent RS-declared datasets without a pin
4. Same, but zero or 2+ agents reference that server → hard error telling you to add agent: the bigcodebench case — previously resolved silently by merge order

2. gym env init generates the decoupled layout. Scaffolded configs declare datasets: on the resources server block; scaffolded benchmark datasets need no agent: pin (the scaffold has exactly one agent referencing the resources server); the generated data .gitignore covers the per-declaration prepared files from #2717. One exception: when scaffolding reuses a verifier from another config file, the dataset stays on the agent block (we don't know the other file's internal keys, so a partial override can't be merged safely — still fully supported).

3. Manifest validation accepts the new layout. gym env validate previously required exactly one dataset-bearing agent; it now equally accepts one dataset-bearing resources server, resolving the agent by the same rules as above.

What does NOT change

Where agent_ref is headed

Same timeline as #2717: this stack removes agent_ref from data files. This PR ensures nothing new is ever generated in the old format; the next PR migrates what exists; the cleanup PR removes the field from the data contract (it remains only in run outputs, as a record of which agent actually ran each row).

Tests

90 passed across benchmark/scaffold suites. New tests pin all four resolution rules and the scaffolded config layout (datasets on the RS block, none on the agent block).

Relationship to other PRs

Part 4 of the dataset↔agent decoupling work (RFC: gym-dataset-preparation, Open Question 1), now an individual PR targeting main (#2710/#2713/#2717 merged). The repo-wide migration in #2732 builds on this PR's resolver and should merge after it.

🤖 Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Aug 24, 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.

@adil-a
adil-a force-pushed the data-prep/benchmarks-scaffold branch from 95a419f to 5d014c0 Compare August 24, 2026 22:33
@adil-a
adil-a force-pushed the data-prep/benchmarks-scaffold branch from 5d014c0 to 39f01c8 Compare August 24, 2026 22:43
@adil-a
adil-a force-pushed the data-prep/benchmarks-scaffold branch from 39f01c8 to b7b7bca Compare August 25, 2026 02:12
@adil-a
adil-a force-pushed the data-prep/benchmarks-scaffold branch from b7b7bca to 3d07e79 Compare August 25, 2026 05:36
@adil-a
adil-a force-pushed the data-prep/benchmarks-scaffold branch from 3d07e79 to 4868e89 Compare August 25, 2026 18:37
@github-actions github-actions Bot added the sla:triage-overdue Review assignment is over the one-business-day SLA label Aug 25, 2026
@anwithk
anwithk requested review from ananthsub and marta-sd August 25, 2026 22:29
@github-actions github-actions Bot removed the sla:triage-overdue Review assignment is over the one-business-day SLA label Aug 25, 2026
@adil-a adil-a added data and removed data labels Aug 26, 2026
@adil-a
adil-a force-pushed the data-prep/benchmarks-scaffold branch from 4868e89 to babadb8 Compare August 26, 2026 02:20
@adil-a

adil-a commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test babadb8

@adil-a

adil-a commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test babadb8

@adil-a

adil-a commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test d94c88e

@adil-a

adil-a commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test bc44b78

@adil-a

adil-a commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 0e751be

Comment thread nemo_gym/benchmarks.py Outdated

dataset = datasets[0]

agent_name = dataset.agent or candidate_agent_server_instance_names[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

take this config:

shared_rs:
  datasets:
    - agent: agent_b

agent_a:
  resources_server: shared_rs

agent_b:
  resources_server: some_other_rs

This line selects agent_b because the explicit pin wins without validating the relationship. Collation later writes only task_source: shared_rs, so runtime follows the actual resources-server edge and selects agent_a.

benchmark listing/preparation → agent_b
actual rollout                → agent_a

we should reject the pin unless the named agent references the declaring resources server

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in fcce242. Resolution now goes through a single shared resolver (resolve_dataset_agent in global_config.py), which rejects any pin that doesn't name the declaring agent itself or an agent referencing the declaring resources server — so this config is now a hard error at discovery, preparation, validation, and dispatch alike. Test: test_rs_declared_pin_must_reference_the_declaring_rs.

Comment thread nemo_gym/cli/eval.py Outdated
)
agent_name = candidates[0]

benchmarks_dict[agent_name] = BenchmarkConfig(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suppose two benchmark declarations both select shared_agent:

benchmark_a → shared_agent
benchmark_b → shared_agent

The assignments become:

benchmarks_dict["shared_agent"] = benchmark_a
benchmarks_dict["shared_agent"] = benchmark_b

where benchmark_b overrides benchmark_a silently

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in fcce242. benchmarks_dict is now keyed by the declaring instance (unique by construction; each instance is limited to one benchmark dataset) instead of the resolved agent, so both declarations are prepared. Test: test_two_declarations_resolving_to_one_agent_both_prepare.

Comment thread nemo_gym/benchmarks.py
@@ -93,10 +105,22 @@ def from_initial_config_dict(

dataset = datasets[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider this configuration:

shared_rs:
  datasets:
    - agent: agent_b

agent_a:
  resources_server: shared_rs

agent_b:
  resources_server: shared_rs

Discovery honors the pin and selects agent_b. Collation then emits only:

{"task_source": "shared_rs"}

At rollout time, resolve_task_sources() asks which agents reference shared_rs. It finds both agent_a and agent_b, no longer has the pin, and rejects the row as ambiguous.

We should propagate the pin into the runtime routing, equivalent to:

agent_map:
  shared_rs: agent_b

along with the discovery -> collate -> rollout test for this case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in fcce242. Rather than materializing an implicit agent_map, resolve_task_sources() now resolves through the same shared resolver, reading the agent: pin back off the declaring instance in the merged config — so this config routes to agent_b end to end (an explicit +agent_map still overrides). Conflicting pins across one instance's datasets are a hard error, since rows carry only task_source. Discovery → collate → rollout test added: test_pin_survives_discovery_collate_and_rollout (real collate stamping, real dispatch resolution).

@adil-a

adil-a commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test

@copy-pr-bot

copy-pr-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

/ok to test

@adil-a, there was an error processing your request: E1

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/1/

@adil-a

adil-a commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test bf8f5fa

adil-a and others added 6 commits August 28, 2026 13:51
…declares datasets on the resources server

A benchmark's score depends on the harness, so the manifest should pin it
explicitly rather than have it inferred from which config block declares the
dataset. Benchmark datasets gain an optional agent: key; resolution order is
explicit pin > declaring agent block (legacy inference, kept for un-migrated
manifests) > unique agent referencing the declaring resources server. An
RS-declared benchmark dataset with 2+ referencing agents and no pin is a hard
error naming the fix (resolves the bigcodebench base-agent/benchmark-agent
ambiguity).

gym env init now scaffolds the decoupled layout, so nothing new is born in the
old format: datasets are declared on the resources server block (kept on the
agent only when reusing a verifier from another config, where the RS
implementation key is unknown); scaffolded benchmark datasets carry the agent:
pin; manifest validation accepts a dataset-bearing resources server; the
generated data .gitignore covers the per-declaration prepared files.

Replay over all 225 runnable config closures: no observable change beyond the
diffs already enumerated for the previous PR in the stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
Caught by the live smoke matrix: gym eval prepare / eval run --benchmark had
its own agent-blocks-only dataset scan, so a benchmark dataset declared on a
resources server block (the decoupled layout) was invisible — prepare never
ran and collation then crashed on the missing file with a raw AttributeError
(BenchmarkDatasetConfig has no registry identifiers).

prepare_benchmark now scans both declaring block types with the same agent
resolution as BenchmarkConfig (explicit agent: pin > declaring agent block >
unique agent referencing the declaring RS), and the missing-file download
branch gives benchmark datasets a clear run-gym-eval-prepare error instead of
the AttributeError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
Caught by the live smoke matrix: the new dataset-level agent: key leaked into
the dataset dump embedded in *_metrics.json, so every committed sidecar
conflicted with freshly computed metrics and benchmark e2e runs refused to
proceed. The pin is routing config, not dataset identity — excluding it keeps
all pre-pin sidecars valid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
… not the default

Every current benchmark config resolves without a pin (declaring agent
block, or unique agent referencing the declaring resources server).
Reword the field description and resolver comment so pins read as the
disambiguator for genuinely ambiguous configs rather than a recommended
default. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
…datasets

The scaffolded config always has exactly one agent referencing the
resources server, so the harness resolves from that edge. The agent
key remains the escape hatch for ambiguous configs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
…validation, and dispatch

Addresses review on #2724:
- An `agent:` pin is now validated everywhere it is honored: it must name the
  declaring agent itself, or an agent referencing the declaring resources
  server. Previously discovery honored any pin while dispatch followed the
  agent -> resources-server edge, silently running a different agent.
- resolve_task_sources reads the pin back off the declaring instance, so a pin
  that disambiguates a shared resources server at discovery also routes its
  collated rows (which carry only task_source) at rollout time.
- prepare_benchmark keys benchmarks by declaring instance instead of resolved
  agent, so two declarations resolving to one agent no longer silently drop
  all but the last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adil Asif <adasif@nvidia.com>
@adil-a

adil-a commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 151f1ed

@adil-a

adil-a commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

@marta-sd closing the loop on this one, late. You were right on both counts.

bigcodebench's two-agent closure is an artifact: its benchmark config defines its own agent instead of _inherit_from-ing the base agent like the other benchmarks do. There is no real use for two agents there.

So the pin is going away: #3209 removes the agent: key entirely. An ambiguous config (several agents on one resources server, dataset on the server) is a load-time error that points to agent_map, fan_out, or declaring the dataset on the agent. A leftover agent: key is also an error, not silently ignored. Nothing in the repo used the key — #2732 already dropped every pin, and the scaffold never emitted one — so behavior is unchanged for every config on main.

The bigcodebench _inherit_from cleanup is a small separate follow-up.

This branch was previously deployed

1 inactive deployment
public — 151f1ed5 Deployed Aug 28, 2026 by copy-pr-bot[bot] via release / finalize / notify #2270
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

r0.6.0 Auto-cherrypick to release branch. Apply before merge; cherrypick happens after merge. sla:review-overdue Review response is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants