Skip to content

feat(fsdp): score policy logprobs over the recorded support under Ulysses SP - #2085

Open
dyurk-lila wants to merge 21 commits into
NovaSky-AI:mainfrom
dyurk-lila:dyurk/ss-fsdp-ulysses
Open

feat(fsdp): score policy logprobs over the recorded support under Ulysses SP#2085
dyurk-lila wants to merge 21 commits into
NovaSky-AI:mainfrom
dyurk-lila:dyurk/ss-fsdp-ulysses

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

PR 32 applies support-conditioned replay to Megatron. An FSDP policy or reference model must compute log-probabilities over the same recorded distribution; otherwise policy ratios and KL terms still compare incompatible normalizations.

This PR makes the replay scorer backend-neutral and wires it through the Hugging Face/FSDP policy, policy-recompute, and reference forwards, including padding removal and Ulysses sequence parallelism.

Shared scorer boundary

The common score_aligned_sample_support helper now accepts already-aligned model outputs, sampled IDs, support row IDs, loss masks, and optional trajectory IDs. Both backends use it for support normalization and the synthetic-EOS full-vocabulary fallback.

Megatron continues to derive trajectory structure from its TokenMetadataLayout. FSDP supplies explicit trajectory IDs after its own padding-removal and sequence-parallel transforms. This keeps backend-specific layout mechanics outside the numerical scorer.

FSDP alignment

The FSDP wrapper builds three token-aligned metadata channels in canonical left-padded coordinates:

  • support row IDs, using -1 where no row exists;
  • the response loss mask; and
  • trajectory IDs for EOS fallback capacity.

Those channels follow the model input through:

  1. removal of left padding with the same nnz_indices used for token IDs;
  2. next-token alignment; and
  3. Ulysses padding and per-rank sequence slicing.

Support row IDs are already placed at the logit positions that predict each response token, so they are not rolled. The loss mask is token-positioned and is rolled by one alongside the sampled targets. Keeping this distinction explicit avoids an off-by-one that can otherwise pass shape checks.

Ulysses metadata support

ulysses_pad_and_slice_inputs now accepts [batch, sequence, ...] metadata rather than assuming a two-dimensional token-ID tensor. Padding is applied along the sequence dimension and accepts a field-specific value: 0 for ordinary IDs and masks, -1 for missing support rows and padding trajectory IDs.

This lets all token-aligned channels take exactly the same sequence shard without constructing a dense support tensor on every rank.

Policy and reference behavior

  • Policy training and policy recomputation use support-conditioned log-probabilities when replay is enabled.
  • Reference forwards use the same support and policy temperature so the KL compares the intended distributions.
  • The replay configuration gate now accepts both megatron and fsdp strategies.
  • When replay is disabled, existing FSDP scoring and reference-temperature behavior are unchanged.
  • CPU logits use the standard Torch log-softmax path even when the optional Flash Attention cross-entropy package is importable.

Testing

  • FSDP tests compare values, gradients, and optimizer updates with direct support-conditioned references.
  • Coverage includes left-padded batches, removed-padding batches, variable-length microbatches larger than one, synthetic EOS, missing support, and feature-disabled behavior.
  • Ulysses tests sweep padding sizes and shard ranks for higher-rank metadata, field-specific sentinels, row IDs, masks, and trajectory IDs.
  • Sequence-parallel tests reconstruct the full result from shards and compare it with the unsharded scorer.
  • Shared scorer tests verify both metadata-layout and explicit-trajectory fallback modes.

Note

High Risk
Changes how policy and reference log-probabilities are computed when replay is on, which directly affects PPO ratios, KL, and gradients. Alignment bugs under packing or Ulysses would silently train against the wrong distribution.

Overview
Makes FSDP/HF scoring match Megatron’s support-conditioned replay: when enable_sample_support_replay is on, policy training, policy recompute, and reference forwards score each token over the recorded sampler top-k (plus a full-vocab fallback for the synthetic EOS) instead of the full vocabulary.

The shared score_aligned_sample_support helper now takes already-aligned logits, sampled IDs, support row IDs, loss masks, and optional trajectory IDs. FSDP builds those channels in left-padded coordinates, then follows the same unpad / next-token / Ulysses shard path as the tokens. Support row IDs stay at logit positions (not rolled); the loss mask is rolled with the targets.

ulysses_pad_and_slice_inputs pads along the sequence dim for higher-rank metadata and accepts per-channel sentinels (-1 for missing support rows). Reference forwards use the same support and policy temperature so KL compares matching distributions. Disabled replay keeps the previous FSDP scoring path.

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

dyurk-lila and others added 21 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 a new feature for sample-support capture in the inference engine, allowing for the renormalization of rollout logprobs over a bounded sampler support set. The changes include updates to the inference server's communication protocol (using packed side-channel arrays), new utilities for managing packed tensors and metadata traces, and updates to the training pipeline to support this new metadata. I have provided feedback on improving the robustness of the network response parsing and adding necessary null checks for configuration parameters.

Comment on lines +223 to +226
try:
raw = await resp.read()
body = load_packed_body(raw) if packed_side_channels else orjson.loads(raw)
except orjson.JSONDecodeError as exc:

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

If load_packed_body(raw) raises a ValueError (which can happen if the response is truncated or corrupted), the exception will propagate immediately and bypass the retry loop. Catching both orjson.JSONDecodeError and ValueError ensures that transient network/transfer issues causing malformed payloads are retried gracefully.

Suggested change
try:
raw = await resp.read()
body = load_packed_body(raw) if packed_side_channels else orjson.loads(raw)
except orjson.JSONDecodeError as exc:
try:
raw = await resp.read()
body = load_packed_body(raw) if packed_side_channels else orjson.loads(raw)
except (orjson.JSONDecodeError, ValueError) as exc:

# Eval requests opt out of capture and do not use these constraints.
if self.generator.inference_engine.enable_return_sample_support_set:
sampling_params = self.generator.sampling_params
if sampling_params.temperature <= 0:

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

If sampling_params.temperature is None, the comparison sampling_params.temperature <= 0 will raise a TypeError. Checking for None explicitly prevents this and ensures a clear ValueError is raised instead.

Suggested change
if sampling_params.temperature <= 0:
if sampling_params.temperature is None or sampling_params.temperature <= 0:

Comment on lines +208 to +213
def load_packed_body(raw: bytes, *, fields: tuple[str, ...] = PACKED_SIDE_CHANNEL_FIELDS) -> dict[str, Any]:
"""Parse a response after replacing registered base64 blobs with views.

Null fields pass through. An envelope layout the scan cannot splice raises
instead of falling back to materializing the base64 as a Python string.
"""

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

The load_packed_body function relies on exact byte-level matching of the JSON payload (assuming compact formatting from orjson). If any intermediate proxy, load balancer, or API gateway reformats or pretty-prints the JSON response in transit, the byte-scan will fail to match, causing _restore_packed_data to raise a ValueError and crash the client. Consider documenting this strict compact-JSON requirement or implementing a more robust fallback parser if intermediate proxies are expected.

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