Repository navigation
feat(rollout): let run_examples callers cancel rollouts still in flight - #4029
Conversation
6836fd4 to
83f4baf
Compare
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>
83f4baf to
dbefd86
Compare
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>
…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>
ananthsub
left a comment
There was a problem hiding this comment.
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)"]
Smaller points, none blocking:
- Collector cleanup. The collector's
finallycloses only the bounded iterator. Checkinghasattr(rows, "aclose")would cover both. It only matters for library callers with a long-lived event loop, sinceasyncio.runcancels 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 hasrun_outcomesreturn 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'sretiredoes (#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.
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
|
LGTM on |
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>
|
/ok to test f28029f |
yuhezhang-ai
left a comment
There was a problem hiding this comment.
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.
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39008712301 |
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>
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>
…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>
run_examples()now exposesaclose()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
c2000f79f6bea21e9b21f7f021d57a06f6082974is integrated additively. The remaining test import conflict is resolved by retainingsignal,sys, andtime; #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:RL #4482 at
778a9d7ee3c8811174745137519748a6b9443271: all 17 native real-Gym interoperability cases passed again against Gym headf28029f5b359858b09596c45a476fc1dd5b52596after 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.