Skip to content

rl_program: prefetch and pack the upcoming groups while the optimizer update runs - #2886

Closed
s-noghabi wants to merge 1 commit into
noghabi/mlperf-send-earlyfrom
noghabi/mlperf-prefetch-next-batch
Closed

s-noghabi wants to merge 1 commit into
noghabi/mlperf-send-earlyfrom
noghabi/mlperf-prefetch-next-batch

Conversation

@s-noghabi

Copy link
Copy Markdown
Collaborator

Stacked on #2885 (base branch is noghabi/mlperf-send-early); the diff shown here is only the prefetch change. Retarget to atwigg/mlperf once #2885 lands.

Problem

The step's final micro-batch (the one that applies the optimizer) was the only train_step never overlapped with anything: _train_loop awaited it, then the checkpoint, the weight-sync snapshot and the commit, and only then pulled step N+1's first groups from scored_q and packed them. Measured on at-sph1-s1a (35B, gbs512), median per step:

segment median
final micro-batch (fwd_bwd + update) 27.1 s
first pack of step N+1 (after commit) 4.1 s

Step N+1's groups are already in scored_q during the update (the [pipeline] ARRIVE lines for the next batch land throughout), so the first pack can happen then instead.

Change

New prefetch_next_batch option (default off). When set, an optimizer-update train_step runs as a task and, while it is in flight, the loop pulls ready groups from scored_q and feeds them to the assembler. Packed micro-batches are held in self._prefetched and consumed first by whichever step (or mini-batch) comes next, which also attributes their rewards/advantages/packing time to it. Nothing is dispatched early: the next micro-batch still waits for the update, the checkpoint, the sync snapshot and the commit.

Details:

  • Under prompt-batch ordering the first fetch draws from the queue's cursor — the rest of the current batch for a mid-batch update, otherwise the next batch (BatchOrderedQueueManager already serves one batch ahead of the committed one by design). Later fetches are pinned to that batch, so the prefetch never crosses a second boundary. When the buffer's head belongs to a later batch than the step's, the step treats it as the end of its batch, exactly as it treats a None from the queue.
  • Stops at the first of: update finished, full_batch_size groups buffered, batch drained, queue closed. Skipped at the end of the last step. The buffer is cleared with the assembler on reset.
  • The in-flight update is registered as _pending_train so teardown cancels it like any other train step; a prefetch failure is surfaced only after the update has returned, so an optimizer step is never left unobserved.
  • _fetch_group / _feed_group factor the existing fetch and pack code so the loop and the prefetch share it (no behaviour change there).
  • Plumbed through run_deepswe_dist.py (--prefetch_next_batch), k8s_launcher.sh (PREFETCH_NEXT_BATCH, default false) and the MLPerf recipes; the gbs512 and gbs1024 submission recipes enable it alongside PIPELINE_TRAIN_MICROBATCHES.

Tests

  • test_prefetch_packs_next_batch_while_optimizer_update_runs (prompt-batch order): step 0's update observes batch 1 fully pulled and packed; each step still trains exactly its own batch and next_batch_idx ends at 2.
  • test_prefetch_skips_the_last_step
  • test_prefetch_feeds_next_group_while_optimizer_update_runs (arrival order)
  • mlperf_submission_launcher_test asserts the flag reaches the orchestrator JobSet.

rl_program_test.py, orchestrator_test.py, config_wiring_test.py (229) pass on CPU. The deepswe_dist launcher tests fail on my VM with and without this change (JobSet render / env dependent).

… update runs

The step's final micro-batch (the one that applies the optimizer) was the
only train_step that was never overlapped with anything: `_train_loop`
awaited it, then the checkpoint, the weight-sync snapshot and the commit,
and only then pulled step N+1's first groups from `scored_q` and packed
them. On at-sph1-s1a (35B, gbs512) the update took ~27 s and the first
pack of the next step another ~4 s, during which the trainer was idle,
even though step N+1's groups had already arrived.

With `prefetch_next_batch=True`, an optimizer-update train_step is run as
a task and, while it is in flight, the loop pulls ready groups from
`scored_q` and feeds them to the assembler. The packed micro-batches are
held in `self._prefetched` and consumed first by whichever step (or
mini-batch) comes next, which also attributes their bookkeeping to it.
Nothing is dispatched early: the next micro-batch still waits for the
update, the checkpoint, the sync snapshot and the commit.

Under prompt-batch ordering the first fetch draws from the queue's cursor
(the rest of the current batch for a mid-batch update, otherwise the next
batch, which the queue already serves one ahead of the committed one);
later fetches are pinned to that batch, so the prefetch never crosses a
second boundary. When the buffer's head belongs to a later batch than the
step's, the step treats that as the end of its batch, as it would a None
from the queue. The prefetch is skipped at the end of the last step, and
the buffer is cleared with the assembler on reset.

The fetch and feed code is factored into `_fetch_group` / `_feed_group`
and shared by the loop and the prefetch. The flag is plumbed through
`run_deepswe_dist.py` (`--prefetch_next_batch`), `k8s_launcher.sh`
(`PREFETCH_NEXT_BATCH`) and the MLPerf recipes, where the gbs512 and
gbs1024 submission recipes enable it alongside
`PIPELINE_TRAIN_MICROBATCHES`.
@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 implements a performance optimization for the RL training loop by overlapping data preparation with the optimizer update step. By pre-packing upcoming groups during the relatively idle time of an optimizer update, the system reduces the time spent waiting for the next micro-batch. The change is configurable via a new flag and includes necessary updates to existing launcher scripts and recipes to ensure seamless integration.

Highlights

  • Prefetching Optimization: Introduced a new prefetch_next_batch option that allows the system to pull and pack upcoming data groups while the optimizer update is in flight, effectively hiding the latency of the next step's initial pack operation.
  • Infrastructure Integration: Plumbed the new prefetch flag through run_deepswe_dist.py, k8s_launcher.sh, and the MLPerf recipes, enabling it by default for standard GBS512 and GBS1024 submissions.
  • Robust Testing: Added comprehensive unit tests in rl_program_test.py to verify correct prefetching behavior, batch attribution, and graceful handling of the final training step.
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 introduces a prefetching mechanism (prefetch_next_batch) to the RL training program, allowing upcoming trajectory groups to be pulled and packed from the scored queue while an optimizer update is in flight. The feedback identifies a critical missing import (contextlib) that would cause a runtime crash, and points out several style guide violations. Specifically, the reviewer recommends using strict type hints instead of Any, avoiding speculative getattr calls on structured schemas to follow the 'one well-lit path' principle, and using asyncio.create_task instead of asyncio.ensure_future for consistency.

Comment on lines +2115 to +2116
with contextlib.suppress(asyncio.CancelledError):
await fetch

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.

critical

The contextlib module is not imported in this file, which will cause a NameError and crash the training loop when a prefetch task is cancelled.

Instead of using contextlib.suppress, use a standard try-except block to handle asyncio.CancelledError safely without requiring additional imports.

Suggested change
with contextlib.suppress(asyncio.CancelledError):
await fetch
try:
await fetch
except asyncio.CancelledError:
pass

"""

batch_idx: int | None
scored_items: list[Any]

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

According to the Tunix Code Review Style Guide (Section 5, Type hints & Strict Typing), we should use strict, specific type hints rather than Any whenever a specific type can be expressed.

Since scored_items contains datatypes.TrajectoryItem objects, we should annotate it as list[datatypes.TrajectoryItem].

Suggested change
scored_items: list[Any]
scored_items: list[datatypes.TrajectoryItem]
References
  1. Use strict, specific type hints: Always annotate function signatures, attributes, and data structures with concrete, specific types rather than Any. (link)


async def _fetch_group(
self, batch_idx: int | None
) -> tuple[int | None, list[Any]]:

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

According to the Tunix Code Review Style Guide (Section 5, Type hints & Strict Typing), we should use strict, specific type hints rather than Any whenever a specific type can be expressed.

Annotate the return type of _fetch_group as tuple[int | None, list[datatypes.TrajectoryItem]] instead of using list[Any].

Suggested change
) -> tuple[int | None, list[Any]]:
) -> tuple[int | None, list[datatypes.TrajectoryItem]]:
References
  1. Use strict, specific type hints: Always annotate function signatures, attributes, and data structures with concrete, specific types rather than Any. (link)

return batch_idx, await self.scored_q.get_group_batch(num_groups=1)

async def _feed_group(
self, scored_items: list[Any]

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

According to the Tunix Code Review Style Guide (Section 5, Type hints & Strict Typing), we should use strict, specific type hints rather than Any whenever a specific type can be expressed.

Annotate the scored_items parameter as list[datatypes.TrajectoryItem] instead of list[Any].

Suggested change
self, scored_items: list[Any]
self, scored_items: list[datatypes.TrajectoryItem]
References
  1. Use strict, specific type hints: Always annotate function signatures, attributes, and data structures with concrete, specific types rather than Any. (link)

"""
payloads = []
for item in scored_items:
payload = getattr(item, "payload", 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

According to the Tunix Code Review Style Guide (Section 5, Error Handling & One Well-Lit Path), we should never use speculative getattr(obj, "field", default) on structured schemas. Instead, we should access attributes directly on a single, strictly typed contract.

Since item is a datatypes.TrajectoryItem and its payload is guaranteed to be set during the critique stage, we should access item.payload directly.

Suggested change
payload = getattr(item, "payload", None)
payload = item.payload
References
  1. One well-lit path (No defensive fallbacks): Never use speculative getattr(obj, "field", default) on structured schemas. Access attributes and keys directly on a single, strictly typed contract. (link)

item.metadata.pop("routed_experts", None)
if isinstance(item.traj, dict):
item.traj.pop("routed_experts", None)
item_payload = getattr(item, "payload", 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

According to the Tunix Code Review Style Guide (Section 5, Error Handling & One Well-Lit Path), we should never use speculative getattr(obj, "field", default) on structured schemas. Instead, we should access attributes directly on a single, strictly typed contract.

Since item is a datatypes.TrajectoryItem and its payload is guaranteed to be set during the critique stage, we should access item.payload directly.

Suggested change
item_payload = getattr(item, "payload", None)
item_payload = item.payload
References
  1. One well-lit path (No defensive fallbacks): Never use speculative getattr(obj, "field", default) on structured schemas. Access attributes and keys directly on a single, strictly typed contract. (link)

return assembled_batches, packing_time_sec

def _has_groups_after(
self, groups_consumed: int, scored_items: list[Any]

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

According to the Tunix Code Review Style Guide (Section 5, Type hints & Strict Typing), we should use strict, specific type hints rather than Any whenever a specific type can be expressed.

Annotate the scored_items parameter as list[datatypes.TrajectoryItem] instead of list[Any].

Suggested change
self, groups_consumed: int, scored_items: list[Any]
self, groups_consumed: int, scored_items: list[datatypes.TrajectoryItem]
References
  1. Use strict, specific type hints: Always annotate function signatures, attributes, and data structures with concrete, specific types rather than Any. (link)

while (
not train_task.done() and len(self._prefetched) < self.full_batch_size
):
fetch = asyncio.ensure_future(self._fetch_group(pinned_batch_idx))

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

For consistency with the rest of the codebase (which uses asyncio.create_task exclusively) and to follow modern asyncio best practices, use asyncio.create_task instead of asyncio.ensure_future.

Suggested change
fetch = asyncio.ensure_future(self._fetch_group(pinned_batch_idx))
fetch = asyncio.create_task(self._fetch_group(pinned_batch_idx))
References
  1. Consistency: Adherence to a consistent style across all projects. (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