Skip to content

perf(r3): collate routes through a container-aware pool, and refuse replay under VPP - #2078

Open
dyurk-lila wants to merge 6 commits into
NovaSky-AI:mainfrom
dyurk-lila:dyurk/r3-pooled-route-collation
Open

perf(r3): collate routes through a container-aware pool, and refuse replay under VPP#2078
dyurk-lila wants to merge 6 commits into
NovaSky-AI:mainfrom
dyurk-lila:dyurk/r3-pooled-route-collation

Conversation

@dyurk-lila

@dyurk-lila dyurk-lila commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Note on the diff: This PR is part of a routed-expert-replay / sampler-support series and builds on the PRs below. GitHub cannot show the intermediate branches here, so the diff is cumulative on top of main — the changes new to this PR sit on top of:

Reviewing in PR order (lowest number first) shows each incremental change cleanly.

Problem

Routed-expert replay arrives as one variable-length NumPy array per trajectory. Collating those arrays into the left-padded global training batch is dominated by allocating and first-touching a large route buffer. The previous serial path also rebuilt padding intermediates and rescanned route IDs even though the inference boundary had already selected a canonical integer dtype.

Separately, replay is unsafe with Megatron virtual pipeline parallelism. Each forward appends a microbatch to every local replay FIFO, while an interleaved model chunk consumes only its own entry during backward. The queues can therefore desynchronize without an immediate crash.

Implementation

  • Add affinity- and cgroup-aware CPU discovery for both cgroup v2 and v1.
  • Size controller-side pools from the usable CPU allocation, reserving capacity for colocated services.
  • Fill disjoint trajectory regions through a bounded thread pool; worker exceptions are propagated to the caller.
  • Allocate the final route tensor once, write canonical route arrays directly, and fill only left-padding and uncaptured suffix rows with valid dummy routes.
  • Reuse the CPU-topology helper for checkpoint delta workers, where the operation is already a barrier and does not need a core reserve.
  • Reject routed-expert replay with virtual pipeline sizes greater than one after Megatron configuration resolution, so provider defaults cannot bypass validation.

The sender remains responsible for route compaction. Collation validates the accepted uint8, int16, and int32 dtypes and promotes a mixed batch to the widest input dtype without another value scan.

Performance

The included packed-route collation benchmark compares identical outputs across serial and pooled implementations. On a 32-core host with OMP_NUM_THREADS=1, a representative 40-layer global batch improved from 29.61 s to 3.35 s (median of five runs).

The result depends on the controller running with one OpenMP thread, as it does under its one-CPU Ray allocation. With unrestricted library threads, the pool can oversubscribe the container and lose performance; the worker cap and cgroup-aware sizing make that dependency explicit.

Testing

  • CPU-topology tests cover affinity, cgroup v1/v2 quotas, unlimited and malformed limits, core reservation, and pool caps.
  • Parallel-fill tests cover serial and parallel execution, disjoint ownership, worker limits, empty inputs, and exception propagation.
  • Preprocessing tests cover route padding, dtype preservation and promotion, non-contiguous arrays, mismatched shapes, and invalid route lengths.
  • Configuration tests cover both explicit and provider-resolved virtual pipeline settings.

dyurk-lila and others added 6 commits August 19, 2026 00:59
Extract the single-request HTTP generation path out of RemoteInferenceClient
into a standalone RemoteGenerateClient plus a RemoteGenerateResult dataclass,
so routed-expert results can be obtained without constructing the full
inference/control-plane client.

RemoteInferenceClient now owns an internal RemoteGenerateClient and delegates
session management, _post, and _generate_single to it. Endpoint routing,
retry/backoff, cache_salt handling, serialization, and lifecycle behavior are
unchanged.
For multi-turn rollouts, build routed-expert (R3) trace data incrementally
as the conversation grows instead of re-gathering the whole conversation's
routes on every turn.

- Add `routed_experts_prompt_starts` to `InferenceEngineInput` and thread a
  per-request `routed_experts_prompt_start` through `RemoteInferenceClient`
  and `RemoteInferenceGenerator.generate()`, forwarded as a sampling param so
  the engine only returns routes for the newly generated suffix.
- Introduce `TokenMetadataTrace` (token-aligned array accumulator) and
  `RoutedExpertTrace`, which records each generation's routes and finalizes a
  full per-token routed-expert array with loss-mask-aware terminal padding.
- Track a `RoutedExpertTrace` on `AgentLoopState` and record routes per turn,
  replacing the previous whole-conversation re-gather in
  `SkyRLGymGenerator.agent_loop`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

@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 incremental routed expert tracking and refactors the remote inference client by extracting a reusable RemoteGenerateClient class. It also adds a CPU topology utility to dynamically size thread pools based on cgroup quotas and process affinity, and parallelizes batch row filling. Feedback on these changes includes catching aiohttp.ClientConnectionError instead of specific exceptions to handle all transient connection failures during POST retries, explicitly resetting _generate_client to None during unpickling to prevent stale states, and catching OSError when querying CPU affinity to ensure compatibility with restricted environments.

Comment on lines +229 to +232
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as exc:
last_exc = exc
logger.debug(f"POST retry {attempt + 1}/{_DATA_PLANE_RETRIES} for {url=}: {exc}")
await asyncio.sleep(1)

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.

high

Catching only ServerDisconnectedError and ClientOSError misses other common transient connection errors like ClientConnectorError (which is raised when a connection is refused, e.g., during a server restart or scale-up). Since ClientConnectorError inherits from ClientConnectionError but not ClientOSError, it will bypass this retry loop entirely. Catching aiohttp.ClientConnectionError instead ensures all connection-related issues are safely retried.

Suggested change
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as exc:
last_exc = exc
logger.debug(f"POST retry {attempt + 1}/{_DATA_PLANE_RETRIES} for {url=}: {exc}")
await asyncio.sleep(1)
except aiohttp.ClientConnectionError as exc:
last_exc = exc
logger.debug(f"POST retry {attempt + 1}/{_DATA_PLANE_RETRIES} for {url=}: {exc}")
await asyncio.sleep(1)

Comment on lines 1502 to 1507
def __setstate__(self, state: dict) -> None:
"""Restore state after unpickling."""
self.__dict__.update(state)
self._session = None
self._gen_sem = None
self._detok_sem = None
self._sem_loop = 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

Explicitly reset _generate_client to None during unpickling to ensure a clean state.

Suggested change
def __setstate__(self, state: dict) -> None:
"""Restore state after unpickling."""
self.__dict__.update(state)
self._session = None
self._gen_sem = None
self._detok_sem = None
self._sem_loop = None
def __setstate__(self, state: dict) -> None:
"""Restore state after unpickling."""
self.__dict__.update(state)
self._generate_client = None
self._gen_sem = None
self._detok_sem = None
self._sem_loop = None

Comment on lines +58 to +61
try:
affinity = len(os.sched_getaffinity(0))
except AttributeError:
affinity = os.cpu_count() or 1

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

In some restricted environments or container runtimes with strict seccomp profiles, calling os.sched_getaffinity(0) can raise an OSError (such as PermissionError). Catching both AttributeError and OSError ensures the fallback to os.cpu_count() is always safe.

Suggested change
try:
affinity = len(os.sched_getaffinity(0))
except AttributeError:
affinity = os.cpu_count() or 1
try:
affinity = len(os.sched_getaffinity(0))
except (AttributeError, OSError):
affinity = os.cpu_count() or 1

@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 using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit e080afe. Configure here.

rollout_expert_indices_out = agent_loop_state.routed_expert_trace.finalize(
token_count=len(prompt_ids) + len(response_ids),
loss_mask=[0] * len(prompt_ids) + loss_mask,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Synthetic EOS breaks route finalize

Medium Severity

With use_conversation_multi_turn=False, a synthetic EOS is appended with loss weight 1 after generation, then RoutedExpertTrace.finalize runs. That finalize rejects loss-active targets past the captured route prefix, so R3 agent loops raise instead of padding the uncaptured EOS the way make_router_padding_mask documents.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e080afe. Configure here.

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