Skip to content

fix(orchestrator): drop restored work that points at deleted by-handle payload files - #4044

Open
faresobeid wants to merge 34 commits into
feat/payload-segmentsfrom
feat/payload-resume-filter
Open

faresobeid wants to merge 34 commits into
feat/payload-segmentsfrom
feat/payload-resume-filter

Conversation

@faresobeid

@faresobeid faresobeid commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #3902 (by-handle payloads) and includes #3970 (checkpoint queued samples and open train groups); review only the commits not in those PRs.

#3970 saves queued samples and the finished episodes of open groups in the orchestrator checkpoint. With by-handle payloads those samples point into payload files, and #3902 deletes a file once the trainer has read its batch. If that happens after the checkpoint was taken, a resume points at deleted files and the trainer crashes (FileNotFoundError in _pread_file).

What

TrainSink.load_state_dict drops restored queued traces and open-group episodes that point at a missing payload file, and logs how many queued traces were dropped. Dropped open-group episodes are regenerated like any other missing group member. Cost: a resumed by-handle run loses the queued work whose files are gone (typically 1-3 batches). Inline runs are unaffected.

Validation

Before merge

Merge #3970 and #3902 first; then this PR's diff is just the resume filter.


Note

Medium Risk
Changes checkpoint format and resume paths for the train pipeline; incorrect restore could lose queued work or crash the trainer, but missing payloads and staleness are explicitly filtered and tested.

Overview
Orchestrator checkpoints now include queued training samples and in-flight rollout groups, saved atomically with progress and curriculum state via TrainSink.state_dict / async CheckpointManager.save. On resume, restored queued traces ship first, open groups continue with the same task/group id, and TrainSource.resumed schedules missing rollouts before new tasks.

Resume safety for by-handle payloads: TrainSink.load_state_dict drops queued traces and saved open-group episodes whose payload files no longer exist (trainer may have read and deleted them after the checkpoint), logs how many queued traces were skipped, and regenerates dropped group members like any other missing rollout. Stale work is still filtered by max_off_policy_steps at restore and when mixed-version resumed groups are scored; batch assembly prefers oldest policy version first.

Supporting wiring: dispatcher emitted_train_groups + forget_consumed_group, snapshot_state on ship/checkpoint, and training.md resume semantics. New unit tests cover missing payload files and off-policy scoring on resumed groups.

Reviewed by Cursor Bugbot for commit 1d222a7. Bugbot is set up for automated code reviews on this repo. Configure here.

…t/payload-resume-filter

# Conflicts:
#	src/prime_rl/orchestrator/train_sink.py
…e payload files

Payload files are deleted once the trainer has read them, which can be after the
checkpoint was taken. On resume, drop queued traces and open-group episodes whose
files are gone instead of crashing the trainer on a missing file.
@faresobeid
faresobeid marked this pull request as ready for review October 13, 2026 17:49

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/prime_rl/orchestrator/train_sink.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/prime_rl/orchestrator/dispatcher.py
# Conflicts:
#	src/prime_rl/orchestrator/orchestrator.py
# Conflicts:
#	tests/unit/orchestrator/test_train_sink.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/prime_rl/orchestrator/train_sink.py Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bcc48f5. Configure here.

rollouts = self.group_size_for(env_name) - len(fresh)
requests.append(
TaskRequest(env_name=env_name, task=task, step=self.progress.step, rollouts=rollouts, group_id=group_id)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Zero-rollout resume requests still dispatch

Medium Severity

load_state_dict always enqueues a TaskRequest, including when rollouts is 0 or negative, and never finalizes a group that already has every member. try_schedule always dispatches one episode for a newly created group, so that request still runs. The extra episode lands in an already-full pending_groups list and process_group then scores an oversized group.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcc48f5. Configure here.

float(sum(len(episode.traces) for episode in group) - sum(len(episode.traces) for episode in fresh)),
)
if fresh:
group = fresh

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale filter undercounts group budget

Low Severity

n_owed is computed after group is replaced with fresh, so a resumed group that lost some members to max_off_policy_steps reports a smaller episode budget than arrived. Zero-output tracking and _record_group then under-count those members.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcc48f5. Configure here.

open_groups = self.dispatcher.open_train_groups()
open_groups |= {request.group_id: (request.env_name, request.task) for request in self.train_source.resumed}
return build_state(self.progress, self.train_source, self.train_sink.state_dict(open_groups))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sweep misses restored queued traces

Medium Severity

oldest_live_payload_version is documented to follow queued work, but it only sees episode_by_trace and pending_groups. Restored queued traces have no episodes, so the orphan payload sweep can treat their v<k> directories as dead and delete files the next trainer read still needs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcc48f5. Configure here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant