Skip to navigation

Aggregate Metrics

View as MarkdownOpen in Claude

After rollout collection, NeMo Gym computes aggregate metrics for each agent by calling the /aggregate_metrics endpoint on the agent server. The results are written to a single _aggregate_metrics.json file.

To compare these saved metrics between runs, or aggregate benchmark scores across models with confidence intervals, see Compare Results. Run aggregation produces the input artifacts; suite comparison combines their selected repeat scores.


How It Works

  1. Rollouts complete — gym eval run --no-serve gathers verify responses (reward + custom fields) for every task/rollout pair.
  2. Group by agent — responses are partitioned by agent name.
  3. Call /aggregate_metrics — for each agent, the stripped verify responses are POSTed to the agent’s /aggregate_metrics endpoint.
  4. Compute stats — per-task and overall statistics (mean, max, min, median, std) are computed for every numeric field. If the resources server overrides compute_metrics() or get_key_metrics(), those are called to add additional metrics.
  5. Compute variability stats — when an agent has two or more repeats, eligible numeric per-repeat metrics are summarized across repeats. See Repeat-Level Metrics.
  6. Write results — all per-agent metrics are written to <output>_aggregate_metrics.json beside the resolved output. For a file symlink, this is normally beside its target; an unambiguous legacy companion layout beside the link is preserved.

With +merge_shards=false, aggregation through a file symlink may update the target’s reports only if that target does not yet exist or is the sole input. Otherwise Gym refuses before changing files. To deliberately replace an existing target’s reports, pass its path directly; to keep those reports, choose a new output path.

Earlier Gym versions could write aggregate metrics beside a symlink while comparison now reads beside its target. After upgrading, aggregate those outputs again before comparing them. Use a new output path to preserve earlier reports, and compare using that same output path. Do not assume an older link-side metrics file is selected.

Explicit failure scoring

Failures are excluded from scores by default. count_failure_classes_as_zero explicitly includes selected failures as zero-valued metric inputs without changing saved outcomes. It matches either the canonical _ng_failure_class or the original producer name saved in _ng_failure_legacy_class, when present. For example, a GDPVal failure with kind gdpval:reference_missing still matches an existing reference_missing selection. Selecting both names counts the task once. Collection and offline aggregation use the same rule; see evaluation resume.

Output Format

The output file is a JSON array with one entry per agent:

[
{
"agent_ref": {"name": "my_agent"},
"agent_metrics": {
"mean/reward": 0.75,
"max/reward": 1.0,
"min/reward": 0.0,
"median/reward": 1.0,
"std/reward": 0.433,
"num_repeats": 2,
"mean_across_repeats/mean/reward": 0.75,
"median_across_repeats/mean/reward": 0.75,
"se_across_repeats/mean/reward": 0.042,
"ci_low_95_across_repeats/mean/reward": 0.66,
"ci_high_95_across_repeats/mean/reward": 0.84
},
"key_metrics": {
"mean/reward": 0.75
},
"group_level_metrics": [
{"mean/reward": 1.0, "sample": {"...": "..."}},
{"mean/reward": 0.5, "sample": {"...": "..."}}
],
"repeat_level_metrics": [
{"_ng_rollout_index": 0, "mean/reward": 0.72, "sem/reward": 0.09, "...": "..."},
{"_ng_rollout_index": 1, "mean/reward": 0.78, "sem/reward": 0.09, "...": "..."}
]
}
]
FieldDescription
agent_refAgent identity ({"name": "..."})
agent_metricsOverall stats, custom metrics, num_repeats (distinct repeat indices), and cross-repeat aggregates
key_metricsHeadline numbers (default: all mean/* entries from agent_metrics)
group_level_metricsPer-task breakdown — one entry per task with stats across that task’s rollouts
repeat_level_metricsPer-repeat breakdown — one entry per _ng_rollout_index with stats across that repeat’s tasks. Empty unless the agent has two or more repeats

Repeat-Level Metrics

A single run is a point estimate. Collecting repeats (--num-repeats) lets you separate a real score difference from sampling noise, and NeMo Gym reports that variability at three levels.

Within a repeat, across tasks

repeat_level_metrics holds one entry per (agent, repeat). It is produced only for agents with two or more repeats — an agent with a single repeat has nothing to compare against and is skipped. When no agent qualifies, the list is empty.

Each entry carries:

FieldDescription
agent_refAgent identity
_ng_rollout_indexWhich repeat this entry summarizes
sample_countTasks with a completed rollout in this repeat
missing_countTasks present in some other repeat but not this one — see the caveat below

plus, for every numeric field (reward, token usage, and any numeric field your verifier returns):

KeyDescription
mean/{field}, median/{field}Central tendency across this repeat’s tasks
std/{field}Sample standard deviation (ddof=1)
sem/{field}Standard error of the mean — std / sqrt(n)
min/{field}, max/{field}Range
p25/{field}, p75/{field}Quartiles
ci_low_95/{field}, ci_high_95/{field}95% confidence interval (Student’s t). Omitted when n <= 1

missing_count is not measured against the expected task list. Its denominator is the set of tasks that completed in at least one repeat, so it only catches tasks that succeeded somewhere else and are absent here.

A task that fails in every repeat never appears in any repeat, so missing_count stays 0 for it. The recovery collector saves reported failures, including kill_shaped failures (such as an interrupted worker), in <output>_failures.jsonl. A process death before an outcome is saved remains unknown in run coverage. A row the dispatch budget never started leaves no row; with +record_omitted_rollouts=true it leaves a sidecar row marked _ng_omitted_rollout: true, which records that it never ran and is not counted as a failure.

To check real coverage, use the numbers that are measured against the materialized inputs: expected_num_rollouts and missing_num_rollouts in group_level_metrics (a task with no saved rollout keeps its row there, with num_rollouts: 0, a reward_profile_completion_pct of 0 and no reward statistics), and the completion summary gym eval profile prints at the end of a run. Suite comparison uses materialized inputs when available and labels planned coverage unverified otherwise.

Across repeats

Eligible numeric point estimates in repeat_level_metrics are summarized across repeats and merged into agent_metrics. Each repeat counts as one observation. This includes mean/{field} and custom benchmark metrics (for example, subtask_accuracy) supplied by compute_repeat_metrics(). Existing statistics and uncertainty estimates, including suffixes such as /p95 and /ci_low_95, remain in repeat_level_metrics without being aggregated again.

KeyDescription
mean_across_repeats/{metric}, median_across_repeats/{metric}Mean and median of the per-repeat metric
std_across_repeats/{metric}, se_across_repeats/{metric}Standard deviation and standard error across repeats
min_across_repeats/{metric}, max_across_repeats/{metric}Range across repeats
ci_low_95_across_repeats/{metric}, ci_high_95_across_repeats/{metric}95% confidence interval of the cross-repeat mean; requires two values

mean_across_repeats/mean/{field} and the per-rollout mean/{field} answer different questions but coincide numerically when every repeat covers the same tasks.

Per task, across repeats

group_level_metrics reports num_rollouts, mean, median, and std per task. It deliberately carries no confidence interval: a CI here would require assuming a distribution for a single task’s repeated outcomes, which are frequently binomial rather than normal, and the Central Limit Theorem does not rescue it because these are raw outcomes rather than averages.

Two cases worth knowing about

Both emit a UserWarning, so you will see them in your terminal:

  • Unequal sample counts across repeats. If a task is missing from some repeats (a crashed rollout, an interrupted run), each repeat’s statistics are computed over a different task set, so they are not directly comparable — and agent_metrics skews toward whichever tasks happened to complete. Collect the missing rollouts before drawing conclusions. Note this fires off missing_count, so it inherits the blind spot above: tasks that failed in every repeat do not trigger it.
  • Zero standard error. If every value in a sample is identical, the confidence interval collapses to the single point (mean, mean). This is reported rather than left null, because SciPy’s t.interval computes an indeterminate ±inf * 0 at scale=0 and returns NaN.

Confidence intervals are unbounded, so on a small number of tasks a 95% CI for a 0–1 reward can extend below 0 or above 1. That is expected for a t-interval and is a signal that you need more tasks or repeats, not a bug.

Where it lands on disk

The same statistics are laid out differently depending on which command produced them:

CommandFileLayout
gym eval run, gym eval aggregate<output>_aggregate_metrics.jsonrepeat_level_metrics nested inside each agent’s object
gym eval profile<rollouts>_repeat_level_metrics.jsonA separate flat list, each entry carrying its own agent_ref

gym eval profile always writes its file, containing [] when there is only one repeat. Cross-repeat aggregates (*_across_repeats/{metric}) live in agent_metrics.


Custom Metrics

Override these hooks on your resources server to add custom metrics.

compute_metrics(tasks)

Receives all verify responses grouped by task. Use this for metrics that need the full dataset — pass@k, confidence intervals, cross-task statistics.

compute_repeat_metrics(tasks)

Receives one repeat grouped by task. Return eligible finite numeric point estimates to include them in cross-repeat aggregates.

get_key_metrics(agent_metrics)

Selects headline numbers from full-run agent_metrics. Default returns all mean/* entries.

Example: pass@k

from nemo_gym.server_runtime import (
BaseVerifyRequest,
BaseVerifyResponse,
SimpleResourcesServer,
)
class MathServer(SimpleResourcesServer):
async def verify(self, body: BaseVerifyRequest) -> BaseVerifyResponse:
# ... verification logic ...
pass
def compute_metrics(self, tasks):
n_tasks = len(tasks)
# pass@k: fraction of tasks where at least one rollout got reward=1
pass_at_k = sum(
1 for rollouts in tasks if any(r["reward"] >= 1.0 for r in rollouts)
) / n_tasks
# pass@1 (average of per-task mean rewards)
pass_at_1 = sum(
sum(r["reward"] for r in rollouts) / len(rollouts)
for rollouts in tasks
) / n_tasks
return {"pass@k": pass_at_k, "pass@1": pass_at_1}
def get_key_metrics(self, agent_metrics):
return {
k: agent_metrics[k]
for k in ("pass@k", "pass@1")
if k in agent_metrics
}

Given 3 tasks with 4 rollouts each (task 0: all correct, task 1: all wrong, task 2: half correct), this produces:

[
{
"agent_ref": {"name": "math_simple_agent"},
"agent_metrics": {
"mean/reward": 0.5,
"max/reward": 1.0,
"min/reward": 0.0,
"median/reward": 0.5,
"std/reward": 0.522,
"pass@k": 0.667,
"pass@1": 0.5
},
"key_metrics": {
"pass@k": 0.667,
"pass@1": 0.5
},
"group_level_metrics": [
{
"mean/reward": 1.0,
"max/reward": 1.0,
"min/reward": 1.0,
"median/reward": 1.0,
"std/reward": 0.0
},
{
"mean/reward": 0.0,
"max/reward": 0.0,
"min/reward": 0.0,
"median/reward": 0.0,
"std/reward": 0.0
},
{
"mean/reward": 0.5,
"max/reward": 1.0,
"min/reward": 0.0,
"median/reward": 0.5,
"std/reward": 0.577
}
]
}
]