Skip to content

rl_program: run end-of-step bookkeeping in the background so step N+1's first micro-batch is sent earlier - #2885

Closed
s-noghabi wants to merge 1 commit into
atwigg/mlperffrom
noghabi/mlperf-send-early
Closed

s-noghabi wants to merge 1 commit into
atwigg/mlperffrom
noghabi/mlperf-send-early

Conversation

@s-noghabi

Copy link
Copy Markdown
Collaborator

Problem

After a step commits, _train_loop ran the deferred trainer get_metrics, the step metric reduction and logging, on_step_end, and gc.collect() + malloc_trim inline on the event loop before pulling step N+1's groups. The trainer sat idle for all of it, and the synchronous reduction/GC also stalled the dispatch, polling and critique stages.

Measured on at-sph1-s1a (35B, gbs512, ASYNC_WEIGHT_SYNC, PIPELINE_TRAIN_MICROBATCHES), median per step boundary:

segment median
metric reduction + logging (commit → "step finished") 1.35 s
gc.collect + malloc_trim (→ "step starting") 1.14 s
deferred/pre-sync get_metrics RPC 1.55 s
first pack of step N+1 (not addressed here; see follow-up PR) 4.13 s

Change

End-of-step bookkeeping now runs as a background task created right after the commit:

  • The deferred get_metrics and the filtered-group drain run immediately.
  • The GIL-bound work (metric reduction, trajectory logging, on_step_end, memory release) waits until the next step has issued its first train_step, so it overlaps trainer compute instead of the next step's first packing. It runs in a worker thread (asyncio.to_thread).
  • _train_loop awaits the task before the next step's optimizer update — the trainer's get_metrics slot (_written_metrics) is overwritten there, so the previous fetch must have landed first — and when the loop ends, so a failure in the task still fails the run. If a step's first micro-batch is also its final one, the gate is released before that await so the two cannot wait on each other.
  • run_async/train_stage cancel the task on teardown, like _pending_train / _pending_sync.

step_time_sec is now measured at commit rather than after the deferred metrics fetch; metrics_fetch_time is still reported separately.

Behavioural note: the >>> step N+1 starting log line (from on_step_begin) can now appear before <<< step N finished / Train step N - loss ..., since those are emitted by the background task.

Tests

  • test_next_step_first_train_step_not_held_by_deferred_get_metrics: step N+1's first micro-batch is issued while step N's get_metrics is still outstanding, and the fetch lands before step N+1's optimizer update.
  • test_on_step_end_runs_after_next_step_issued_its_first_train_step
  • test_on_step_end_failure_fails_the_run
  • test_step_metrics_reduce_off_the_event_loop

tests/experimental/orchestrator/rl_program_test.py (186) and orchestrator_test.py pass on CPU. The deepswe_dist launcher/reaper tests (mlperf_submission_launcher_test, prepull_sandbox_images_test, cluster_reaper_test) fail on my VM with and without this change (JobSet render / env dependent); they do not import rl_program.

…'s first micro-batch is sent earlier

After a step commits, `_train_loop` used to run the deferred trainer
`get_metrics`, the step metric reduction and logging, `on_step_end`, and
`gc.collect()` + `malloc_trim` inline on the event loop before pulling the
next step's groups. The trainer sat idle for all of it. On at-sph1-s1a
(35B, gbs512) this was ~2.5 s per step boundary on top of packing, and the
sync reduction and GC also stalled the dispatch/polling stages.

This moves that work into a background task created right after the commit:

* The deferred `get_metrics` and the filtered-group drain run immediately.
* The GIL-bound work (metric reduction, trajectory logging, `on_step_end`,
  memory release) waits until the next step has issued its first
  `train_step`, so it overlaps trainer compute rather than the next step's
  first packing. It runs in a worker thread.
* `_train_loop` awaits the task before the next step's optimizer update
  (the trainer's `get_metrics` slot is overwritten there, so the previous
  fetch must have landed) and when the loop ends, so a failure in it still
  fails the run. If a step's first micro-batch is also its final one, the
  gate is released before that await so the two cannot wait on each other.

`step_time_sec` is now measured at commit rather than after the deferred
metrics fetch; `metrics_fetch_time` is still reported separately.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request optimizes the training loop by decoupling end-of-step bookkeeping from the main execution path. By offloading metrics fetching, reduction, and memory management to background tasks, the system can initiate the next training step's micro-batch earlier, significantly reducing idle time for the trainer. The changes ensure that these background operations are synchronized with the trainer's progress and that any failures are handled gracefully to maintain run integrity.

Highlights

  • Background Bookkeeping: Moved end-of-step bookkeeping (metrics fetch, reduction, logging, and memory release) to a background task to allow the next step's first micro-batch to be processed without waiting for these operations.
  • GIL-Bound Work Optimization: Offloaded GIL-bound tasks like metric reduction and memory release to a separate thread using asyncio.to_thread, ensuring they overlap with trainer compute rather than stalling the event loop.
  • Task Synchronization: Implemented an asyncio.Event to gate GIL-bound bookkeeping until the next step issues its first train_step, preventing resource contention.
  • Reliability: Added explicit task awaiting and failure logging to ensure that any errors in the background bookkeeping tasks correctly propagate and fail the training run.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request refactors the end-of-step bookkeeping in StandardRLProgram to run as a background task, allowing the next step's first micro-batch to be issued immediately without waiting for metrics fetching, reduction, logging, or host memory release. It also offloads metric reduction and host memory release to background threads. Feedback on these changes suggests improving type safety in the newly introduced _finish_step_bookkeeping method by replacing Any annotations with concrete, specific types in accordance with the repository's strict typing guidelines.

Comment on lines +2375 to +2399
async def _finish_step_bookkeeping(
self,
*,
current_step: int,
step_result: Any,
trainer_metrics: Any,
pending_metrics_fetch: bool,
all_step_items: list[Any],
uncommitted_groups: list[Any],
scored_items: list[Any],
step_rewards: list[float],
step_advantages: list[float],
generation_metrics: dict[str, Any],
num_rollouts: int,
num_microbatches: int,
step_padding_stats: list[batch_assembly.PaddingStats],
step_packing_time_sec: float,
step_time_sec: float,
consumed_policy_version: int,
step_sampler_agreement: dict[str, tuple[Any, list[float]]],
policy_training_time: float,
exposed_generation_time: float,
weight_sync_time: float,
policy_version: int,
) -> None:

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.

medium

To adhere to the repository style guide on strict typing, we should avoid using Any in the signature of _finish_step_bookkeeping where more specific types can be expressed. Specifically:

  • all_step_items can be typed as list[datatypes.TrajectoryItem]
  • uncommitted_groups can be typed as list[list[datatypes.TrajectoryItem]]
  • scored_items can be typed as list[datatypes.TrajectoryItem]
  • generation_metrics can be typed as dict[str, float]
  • step_sampler_agreement can be typed as dict[str, tuple[Callable[[list[float]], float], list[float]]]
  async def _finish_step_bookkeeping(
      self,
      *,
      current_step: int,
      step_result: Any,
      trainer_metrics: Any,
      pending_metrics_fetch: bool,
      all_step_items: list[datatypes.TrajectoryItem],
      uncommitted_groups: list[list[datatypes.TrajectoryItem]],
      scored_items: list[datatypes.TrajectoryItem],
      step_rewards: list[float],
      step_advantages: list[float],
      generation_metrics: dict[str, float],
      num_rollouts: int,
      num_microbatches: int,
      step_padding_stats: list[batch_assembly.PaddingStats],
      step_packing_time_sec: float,
      step_time_sec: float,
      consumed_policy_version: int,
      step_sampler_agreement: dict[str, tuple[Callable[[list[float]], float], list[float]]],
      policy_training_time: float,
      exposed_generation_time: float,
      weight_sync_time: float,
      policy_version: int,
  ) -> None:
References
  1. Always annotate function signatures, attributes, and data structures with concrete, specific types rather than Any or untyped dict/object. Avoid Any whenever a specific type can be expressed. (link)

@s-noghabi

Copy link
Copy Markdown
Collaborator Author

Superseded by #2901, which rebuilds the send-early / background-bookkeeping change on top of #2877.

@s-noghabi s-noghabi closed this Oct 15, 2026
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.

2 participants