Skip to content

feat(rollout): let run_examples callers cancel rollouts still in flight - #4029

Merged
linj-glitch merged 8 commits into
NVIDIA-NeMo:mainfrom
linj-glitch:linj/run-examples-aclose
Oct 17, 2026
Merged

linj-glitch merged 8 commits into
NVIDIA-NeMo:mainfrom
linj-glitch:linj/run-examples-aclose

Conversation

@linj-glitch

@linj-glitch linj-glitch commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

run_examples() now exposes aclose() so direct callers and collection owners can cancel unfinished client work. aclose_and_drain() stops admission before suspending and preserves completed, unconsumed outcomes for deadline consumers such as RL #4482. Completed failures remain observable; consumed payloads are released; outcomes are delivered once.

Concurrent eager consumers now use one queue notification per finished rollout. Drain wakes every parked consumer. A deterministic public-API regression at 1,024 individually arriving outcomes measures 1,024 consumer resumptions, versus 524,800 with the shared event. Tests also cover cancelled notified consumers, multiple parked drain consumers, completed backlogs, exception retrieval, and result lifetime.

Both lifecycle methods wait for local client tasks. They do not acknowledge server/agent cleanup or guarantee sandbox release. Existing sequential use remains compatible; no configuration or benchmark-score behavior changes.

Main c2000f79f6bea21e9b21f7f021d57a06f6082974 is integrated additively. The remaining test import conflict is resolved by retaining signal, sys, and time; #4646's asynchronous artifact reads are preserved. Collector cleanup retains main's timing tasks, SIGTERM restoration, and report, while closing both iterator modes. Its capture-write barrier and reward telemetry are preserved. The queue-based iterator also preserves #4644's consumed-result release; all four new upstream weak-reference cases pass unchanged.

Validation on head f28029f5b359858b09596c45a476fc1dd5b52596:

python3 -m pytest -o addopts= -q \
  tests/unit_tests/test_rollout_collection.py \
  tests/unit_tests/test_server_utils.py \
  tests/unit_tests/telemetry/test_observer.py \
  tests/unit_tests/test_run_timing.py \
  tests/unit_tests/test_base_responses_api_model.py \
  tests/unit_tests/telemetry/test_rollout_reward.py \
  tests/unit_tests/test_rollout_health.py \
  tests/unit_tests/test_rollout_observability.py \
  tests/unit_tests/test_rollout_correlation.py \
  tests/unit_tests/test_token_id_capture.py \
  tests/unit_tests/test_trajectory_builder.py
# 1,244 passed (including all 498 rollout tests)

pre-commit run --all-files
# All applicable hooks passed; no files modified. The guardwords hook retains its catalog-availability guard.

RL #4482 at 778a9d7ee3c8811174745137519748a6b9443271: all 17 native real-Gym interoperability cases passed again against Gym head f28029f5b359858b09596c45a476fc1dd5b52596 after the main integration. The original three performance regressions failed before the queue fix and passed afterward. Disabling drain shutdown also makes the new parked-consumer regression fail. Real HTTP client cancellation remains covered.

Model rollout evidence: N/A for this shared-library lifecycle repair. No training A/B was run. A separate issue is unnecessary because this addresses the P2 feedback on this PR.

#3302 should follow this change and retain both lifecycle methods. RL activation needs a subsequent Gym pin update. Ananth's concrete review requests are addressed. Full mirrored CI and the repository's required approval remain outstanding.

@copy-pr-bot

copy-pr-bot Bot commented Oct 4, 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.

@yaoyu-33 yaoyu-33 added area:evaluation Evaluation, rollout collection, reward profiling, comparison, and exporters complexity:medium Single-domain change with interacting parts or a moderate review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer labels Oct 5, 2026
@linj-glitch
linj-glitch marked this pull request as draft October 5, 2026 06:11
@linj-glitch
linj-glitch force-pushed the linj/run-examples-aclose branch from 6836fd4 to 83f4baf Compare October 8, 2026 00:15
run_examples returned a plain map over the completion iterator, so a
direct caller (NeMo-RL's NemoGym actor, for one) had no way to stop a
batch early: the docstring said as much ("Stopping iteration early leaves
up to max_resident_tasks tasks running because this mapped iterator has no
aclose()"), and in the default unbounded mode the tasks were hidden inside
a generator closure. A trainer that wants to give up on the last few slow
rollouts of a batch (for example one sandboxed agent session running for an
hour after the other 99% of the batch is back) could only abandon the
awaitables while the /run requests, and the sandboxes behind them, kept
running.

run_examples now returns a small iterator object with the same
iteration behavior plus an async aclose(): it cancels every rollout task
still running, which closes its /run request (the server's
ClientDisconnectCancellationMiddleware then cancels the handler), waits for
the cancellations to land, and ends iteration. The unbounded mode's
generator becomes _EagerCompletionIterator (same input-order start and
as_completed order) so both modes support it; the bounded mode already had
aclose().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
@linj-glitch
linj-glitch force-pushed the linj/run-examples-aclose branch from 83f4baf to dbefd86 Compare October 8, 2026 00:20
linj-glitch added a commit to linj-glitch/RL that referenced this pull request Oct 13, 2026
The straggler cutoff needs the iterator returned by
RolloutCollectionHelper.run_examples to have an async aclose(), which
NVIDIA-NeMo/Gym#4029 adds. The pinned Gym (17abf2dd) returns a plain map,
so with it the actor warns once and runs uncut; the pin follows once the
Gym change is on main. Also spells out what cancellation does on the Gym
side, the per-actor scope under sharding, the receipt-mode fallback, and
how cut rows are treated statistically: zero loss, excluded from the
rollout-level statistics over model outputs, but a reward of 0 that still
enters the group baseline/std on current main until the masked-reward
participation policy (NVIDIA-NeMo#4490 / NVIDIA-NeMo#4160) lands. The timing replay is an
estimate of wall time saved, not a convergence result.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
linj-glitch added a commit to linj-glitch/RL that referenced this pull request Oct 13, 2026
…utoff

Drive _iter_gym_results_with_straggler_cutoff, as imported, against the
installed NeMo Gym's real RolloutCollectionHelper.run_examples through a fake
server client whose /run handlers finish after a delay or hang until cancelled.
Three scenarios (unbounded grace cut, bounded grace cut, bounded max_wall cut
with unadmitted rows) check that the hanging handlers observe CancelledError,
finished rows come first and cut rows after them with no result, the iterator
is closed, no task is left pending, and nothing finishes or is admitted late.

Marked nemo_gym and skipped until the installed Gym's iterator has aclose()
(NVIDIA-NeMo/Gym#4029), so the pin bump makes the cutoff path run in the
nemo_gym CI shard without further changes. Passes against the Gym#4029 head
(dbefd8679) and skips on the pinned 17abf2dd.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
@linj-glitch
linj-glitch marked this pull request as ready for review October 13, 2026 23:08

@ananthsub ananthsub left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for this. It fills a real gap: a direct caller such as a trainer's straggler cutoff has no way today to withdraw /run requests it already sent, and #3302 does not cover it because run_examples there still returns a map. I checked the mechanism end to end against a real server: cancelling the task closes the connection, the disconnect middleware cancels the server handler within a millisecond, and nothing retries the cancelled request. The new tests fail on the old code.

Two changes I'd like before merge, both inline: the unbounded aclose() should retrieve every task, and the docstrings should say what aclose() actually waits for. Unit and server tests have not run on this head yet (only lint, sign-off and secrets checks ran), so those need to pass too.

flowchart LR
    A["caller: aclose()"] --> B["cancel unfinished rollout tasks"]
    B --> C["gather tasks in this process"]
    C --> D["aclose() returns"]
    B -. "connection closed" .-> E["server: disconnect middleware cancels handler"]
    E -. "after aclose() returns" .-> F["agent cleanup (depends on agent; some keep running)"]
Loading

Smaller points, none blocking:

  • Collector cleanup. The collector's finally closes only the bounded iterator. Checking hasattr(rows, "aclose") would cover both. It only matters for library callers with a long-lived event loop, since asyncio.run cancels leftover tasks at shutdown.
  • Order with #3302. The two PRs conflict in _run_examples_with_metadata. I'd land this one first; #3302 then keeps its exception-retrieving done callback and has run_outcomes return the closable iterator.
  • End-to-end test. The tests mock the post. One test against a real server with the disconnect middleware would pin down the behavior the docstring describes.
  • Cancel, not retire. aclose() withdraws requests; it does not stop the attempt everywhere and confirm, which is what the checkpoint stack's retire does (#3882). That is the right primitive to ship here. It does make the open question concrete of whether a retire without checkpointing should exist, which I'll pick up on the failure-handling tracker.

Comment thread nemo_gym/rollout_collection.py Resolved
Comment thread nemo_gym/rollout_collection.py Resolved
Comment thread nemo_gym/rollout_collection.py Resolved
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Comment thread nemo_gym/rollout_collection.py Resolved
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
@yuhezhang-ai

Copy link
Copy Markdown
Contributor

LGTM on e24bc02. Both findings from my review are addressed, and all 496 rollout tests pass locally. Please resolve the remaining import conflict (keeping time, signal, and sys) and rerun the required checks on the resulting commit before merging.

Resolve the rollout test import conflict while retaining signal, sys, and time. Preserve main's artifact-read scheduling and existing lifecycle regressions.

Validation: 1,244 focused tests passed; pre-commit run --all-files passed.
Signed-off-by: Lin Jia <linj@nvidia.com>
@yuhezhang-ai

Copy link
Copy Markdown
Contributor

/ok to test f28029f

@yuhezhang-ai yuhezhang-ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM on f28029f5. Rechecked after the conflict resolution: both review findings remain fixed, all 498 rollout tests pass locally, the 1,024-row workload produces 1,024 consumer resumptions, and the memory-retention check retains 0/3 payloads after exhaustion and close. The import conflict is resolved. Full CI has been requested and must pass before merge.

@nemo-automation-bot
nemo-automation-bot Bot requested a review from a team October 16, 2026 14:46
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39008712301

Merged via the queue into NVIDIA-NeMo:main with commit b3103ea Oct 17, 2026
48 of 53 checks passed
@linj-glitch
linj-glitch deleted the linj/run-examples-aclose branch October 17, 2026 06:25
yuhezhang-ai added a commit that referenced this pull request Oct 19, 2026
Rebase the final #3302 implementation onto main after #4029 and #4178. Preserve cancellation and timing cleanup, remove retired Stirrup migration paths, forward GDPVal retry progress, and allow selected-source judge recovery into separate output. Verify saved rewards reach the live counter exactly once.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
yuhezhang-ai added a commit that referenced this pull request Oct 19, 2026
Rebase the final #3302 implementation onto main after #4029 and #4178. Preserve cancellation and timing cleanup, remove retired Stirrup migration paths, forward GDPVal retry progress, and allow selected-source judge recovery into separate output. Verify saved rewards reach the live counter exactly once.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
mikemikimike pushed a commit to mikemikimike/Gym that referenced this pull request Oct 31, 2026
…A-NeMo#3302)

* feat(eval): persist outcomes and resume unfinished evaluations

Rebase the final NVIDIA-NeMo#3302 implementation onto main after NVIDIA-NeMo#4029 and NVIDIA-NeMo#4178. Preserve cancellation and timing cleanup, remove retired Stirrup migration paths, forward GDPVal retry progress, and allow selected-source judge recovery into separate output. Verify saved rewards reach the live counter exactly once.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): preserve retry and scoring compatibility

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(gdpval): isolate HTTP recovery fixtures from core tests

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): preserve managed recovery through explicit resubmission

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): align tracing fixtures with managed recovery

Return a valid scored completion from the incoming telemetry fixture. Verify that resuming a completed run neither sends requests nor emits duplicate rollout dispatch traces.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): align recovery health and retry reporting

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): mark exhausted resume chains incomplete

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): align recovery checks with saved run policy

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): accept the health span context in the native-failure health test

Main's NVIDIA-NeMo#5052 passes the dispatch span context to _record_rollout_health as a fourth argument. Give this test's stub the same optional parameter as main's other stubs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): accept typed judge failures and keep failure paths and legacy classes

NVIDIA-NeMo#5072 lets judge_failsafe name the observed cause as its failure_kind (for
example judge_rate_limited) while the routing class stays judge_failed, and
adds a diagnostic _ng_failure_path. Its base Environment Server also fills
in a kind such as provider_unavailable when the producer left it unset.

- _episode_record() accepts the judge markers and the path on a result whose
  class is judge_failed and whose kind is a judge kind, not only judge_failed
  itself. Such a result stays a retryable judge failure that
  --judge-failed-only re-judges, instead of a terminal
  environment_protocol_violation (or an aborted run without sidecar routing).
- _sanitize_legacy_result() and the native failure branch of _episode_record()
  keep _ng_failure_path on the saved row as a diagnostic, outside the typed
  record.
- An Environment Server failure with a kind keeps environment_server_failed as
  its legacy class, so count_failure_classes_as_zero and the other class
  selectors still match it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): reply with the dispatched episode identity in the provenance tests

Main's provenance tests answered every row with a fixed or malformed episode id. Collection validates a reply against the episode it dispatched, so answer with that identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): keep a legacy link's companions when checking health

Health resolved every rollout path to the file it links to, so a legacy run
reached through a file link lost the sidecar and inventory kept beside the
link. Aggregation and the shard glob keep the link's name in that case
(resolve_rollout_path), so a recheck counted less than the aggregation it
checks. Resolve health paths with the same reader rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): align main's aggregation tests with the run-history merge rules

NVIDIA-NeMo#4996's tests assumed main's aggregation, which merges a run onto itself and
into a directory holding a run. This branch refuses both, because merged rows
would replace rows the run's history indexes.

- Merging a run onto itself, directly or through a linked directory, is
  refused before anything is written; merge_shards=false still recomputes its
  metrics in place.
- A fresh run into an aggregated output drops its shard list; a resume finds
  no run history there and stops before writing.
- Aggregating into a run with history is refused and changes nothing.
- A file link's companions are read beside the link when they sit there,
  otherwise beside the file it resolves to, which owns them, as do a linked
  output's shard list and reports.
- A failed publication leaves the original rows and a shard list that names
  contents that never landed, so a recheck refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): format the omitted-rollout test parameters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(arena): also limit final judge-outage failures

With the run history, an outage counts as a final failure once its retries
are used up, so the not-final limit NVIDIA-NeMo#5073 set would stop catching it. Each
Arena benchmark now sets the final-failure limit to the same share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): accept NVIDIA-NeMo#5072's judge failure report on judge_failed results

NVIDIA-NeMo#5072's judge_failsafe now attaches a failure report instead of a failure
path: `_ng_failure_events`, `_ng_failure_id` and, when events were left out,
`_ng_failure_events_omitted`. NVIDIA-NeMo#3302 rejected those as collector-owned keys,
so a judge outage either aborted collection or became a terminal
environment_protocol_violation that lost the saved answer.

A judge_failed result may now carry those three keys. They stay on the row
as diagnostics, beside the version-1 failure record and not inside it, and
legacy agents keep them too. The unused `_ng_failure_path` handling is
removed, and other collector-owned keys stay forbidden.

The judge fixtures use the row NVIDIA-NeMo#5072's code produces for a judge 429.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* refactor(eval): name the saved failure record RolloutFailureRecord

The failure-boundary design names the saved envelope RolloutFailureRecord:
a run's identity plus the failure data, apart from the runtime exceptions
that report failures. The record, its version and its fields are
unchanged. `RolloutFailure` stays importable as an alias of the same class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): mark the judge report's event ids for the secrets detector

The event ids are random hex from NVIDIA-NeMo#5072's judge_failsafe output, which detect-secrets reads as high-entropy
strings. Name each once with the repository's allowlist pragma.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): mark incomplete CLI runs as failed in tracking

Preserve the final saved-coverage gate after merging NVIDIA-NeMo#5213, and mark its rejected completions as failed for exporters. Keep SIGTERM and retryable/exhausted exit codes unchanged. Cover both CLI paths and tolerated partial runs with a real local MLflow store.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): integrate main's health layering and partial-stage checks

Move collection health reporting below the CLI layer and pass shutdown state
from CLI callers. Register the recovery modules in main's import contract,
without adding dependency exceptions, and refresh the import inventory for
three private helpers intentionally replaced by the recovery implementation.

Cover accepted GDPVal partial stages followed by a still-incomplete stage,
including a no-progress resume. Accepted omissions must not be counted twice.
Align the older GDPVal test with the stage-aware partial-acceptance report.

Validation: 2,707 targeted tests and all-files pre-commit passed. The new
GDPVal regression fails under a controlled double-credit mutation.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(imports): finish recovery compatibility with package moves

Expose the existing ConfigError and ENVIRONMENT_SERVER_FAILED definitions
through the package APIs used by the conformance runner and legacy adapter.
Update the remaining recovery test and conformance import paths. No failure,
scoring, or retry policy changes.

Validation: 3,197 targeted tests and all-files pre-commit passed, including
package interfaces, import layering, recovery, legacy adapter and conformance.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(eval): follow verification and runtime package moves

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* fix(eval): preserve run identity and saved recovery policy

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

* test(swe-pivot): align runtime stub with public imports

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>

---------

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
public — f28029f5 Deployed Oct 16, 2026 by copy-pr-bot[bot] via release / finalize / notify #5641
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:medium Single-domain change with interacting parts or a moderate review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants