Skip to content

perf(eval): tune garbage collection in the rollout driver - #4643

Open
ananthsub wants to merge 1 commit into
mainfrom
ananthsub/eval-driver-gc-tuning
Open

ananthsub wants to merge 1 commit into
mainfrom
ananthsub/eval-driver-gc-tuning

Conversation

@ananthsub

@ananthsub ananthsub commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What changed and why

The rollout collection driver that gym eval run starts runs every rollout's dispatch and result handling on one asyncio event loop, so each Python garbage collection pauses all of them at once. Python's default thresholds are (700, 10, 10). In the CPU capacity simulator, a long-context run with rollout observability on makes the driver on main run about 1,460 collections, with about 2 seconds of total pause and single pauses of about 400 ms. Most of that pause time comes from a handful of full (generation 2) collections, each of which scans every object the driver holds.

RolloutCollectionConfig gains two Hydra-configurable fields, and both gym eval run entry points (with and without --no-serve) apply them to their own process once, before collection starts:

  • driver_gc_freeze (default true) runs one full collection and calls gc.freeze(), so later collections no longer scan the modules, config, and clients loaded at startup.
  • driver_gc_thresholds (default [50000, 50, 100]) is passed to gc.set_threshold(). A value below 1 is rejected by config validation, because a zero turns off automatic collection.

Setting ++driver_gc_freeze=false ++driver_gc_thresholds=null keeps Python's defaults. The settings are applied by _tune_driver_gc in nemo_gym/cli/eval.py, never by RolloutCollectionHelper, so library callers of run_examples such as NeMo RL keep their own garbage-collection state. Resources, agent, and model servers run in their own processes and are unaffected. Without --no-serve, the head server runs inside the driver's process and so shares its settings.

The defaults come from a local experiment that applied the same freeze and thresholds through environment variables. A history pass over nemo_gym/cli/eval.py and RolloutCollectionConfig found no earlier garbage-collection tuning or reason against it, and the only gc use in nemo_gym is the telemetry observer that reports gym.gc.* metrics.

The option is documented next to the other rollout collection settings in fern/versions/latest/pages/reference/cli-commands.mdx.

Because the no-serve entry point now changes process-wide state, an existing test that runs it through main() would leave a frozen heap and raised thresholds in its pytest worker. A new autouse fixture in tests/conftest.py unfreezes and restores the thresholds after every test.

Issue

No issue: this is a focused performance change driven by the capacity simulator measurements below.

Compatibility

gym eval run now changes the driver's garbage-collection settings by default. Rollout outputs and metrics are unchanged. Memory held by reference cycles is freed less often, and the two fields above restore the previous behavior.

Validation

  • PYTHONPATH=. .venv/bin/python -m pytest -q tests/unit_tests/test_cli_eval.py covers the no-serve and serving entry points applying the default thresholds and freeze before collection starts, configured thresholds being applied, the disabled settings leaving garbage collection untouched, and invalid thresholds being rejected. The entry-point tests fail when nemo_gym/cli/eval.py is reverted to main.
  • PYTHONPATH=. .venv/bin/python -m pytest -n 16 -q tests/unit_tests --ignore=tests/unit_tests/test_exporters.py in a venv with the dev and telemetry extras. The only failures are in test_opensandbox_compose.py, test_reward_profile.py, and test_sandbox_stop_retry.py, which depend on the local environment and fail the same way on main.
  • pre-commit run on the changed files.

Rollout evidence

N/A for model rollouts: the change only adjusts the driver process's garbage-collection settings and does not change any agent, verifier, or model server behavior.

Capacity simulator comparison against main at 98ffb644f9, which already includes #4644 (release finished rollout tasks) and #4646 (aggregate collected rollouts off the event loop). The production topology ran 4,096 rollouts per run (64 repeats, 1,024 in flight, 80 history messages of 2,000 characters, observability and model-call capture on), with gym eval run --no-serve as the driver. tuned_off is this branch with ++driver_gc_freeze=false ++driver_gc_thresholds=null. Every run completed all rollouts with none failed. Values come from the driver's last console metric export.

Run gym.gc.collections_total Pause count Pause sum Longest pause gym.event_loop.lag_ms mean gym.event_loop.lag_ms max
main 1 1,464 1,458 2.01 s 385 ms 65 ms 2,295 ms
main 2 1,467 1,455 2.08 s 410 ms 55 ms 1,690 ms
this branch 1 60 60 0.49 s 245 ms 17 ms 888 ms
this branch 2 60 60 0.46 s 222 ms 39 ms 1,789 ms
tuned_off 1 1,463 1,449 2.08 s 405 ms 53 ms 2,006 ms
tuned_off 2 1,458 1,444 2.02 s 388 ms 47 ms 1,583 ms

The defaults cut driver collections by about 24 times and total pause time by about 4 times. With the settings turned off, the branch matches main.

By generation, main runs about six full collections that account for most of its pause time, each taking 120 to 385 ms. This branch runs no full collection. It runs 59 young (generation 0) collections of at most 11 ms each and one generation 1 collection of 222 to 245 ms. That one pause remains because survivors of 50 young collections accumulate in generation 1 before it is collected, and in one run it landed while rollouts were still in flight. A lower second threshold would split it into several shorter pauses, which can be tuned separately with driver_gc_thresholds.

The longest event-loop lag stays above 800 ms in every run, well above any garbage-collection pause on this branch, so other work on the event loop dominates the worst lag. The shared machine was busy during these runs, and lag varied widely between repeats of the same variant.

Running the earlier local experiment, which applied the same freeze and thresholds through environment variables, under the same settings gives the same result as this branch: 60 collections, 0.47 s of total pause, and a longest pause of 222 ms.

@ananthsub ananthsub added feature New capabilities, enhancements, or enablement work area:evaluation Evaluation, rollout collection, reward profiling, comparison, and exporters labels Oct 15, 2026
@nemo-automation-bot
nemo-automation-bot Bot requested review from a team October 15, 2026 18:14
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

@yaoyu-33 yaoyu-33 added complexity:low Localized change in one scope with a small, straightforward review surface needs-review PR is ready for code review and waiting on a reviewer labels Oct 15, 2026
The rollout collection driver runs every rollout's dispatch and result handling on one asyncio
event loop, so each Python garbage collection pauses all of them at once. With Python's default
thresholds of (700, 10, 10), a long-context run with many rollouts in flight collects often, and
the longest pauses show up directly in the driver's gym.event_loop.lag_ms.

Add two RolloutCollectionConfig fields that gym eval run applies to its own process before
collection starts, with and without --no-serve. driver_gc_freeze, on by default, runs one full
collection and calls gc.freeze(), so later collections skip the objects loaded at startup.
driver_gc_thresholds, (50000, 50, 100) by default, is passed to gc.set_threshold(). Setting
driver_gc_freeze=false and driver_gc_thresholds=null keeps Python's defaults. A threshold below 1
is rejected, because a zero turns off automatic collection.

Only the gym eval run entry points apply the settings. Resources, agent, and model servers run in
their own processes, and library callers of run_examples, such as NeMo RL, keep their own
garbage-collection state.

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

This branch was previously deployed

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

Labels

area:evaluation Evaluation, rollout collection, reward profiling, comparison, and exporters complexity:low Localized change in one scope with a small, straightforward review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer 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.

2 participants