Skip to content

refactor(unified_trainer): extract step merging into a shared backend-agnostic module#576

Open
listar2000 wants to merge 1 commit into
mainfrom
refactor/common-step-merge
Open

refactor(unified_trainer): extract step merging into a shared backend-agnostic module#576
listar2000 wants to merge 1 commit into
mainfrom
refactor/common-step-merge

Conversation

@listar2000

Copy link
Copy Markdown
Collaborator

Summary

Both the Tinker and Verl transforms used to implement near-identical cumulative-prefix step merging in backend-specific code, with the same delta-extension loop accessing slightly different fields. This PR pulls the merge into one place behind a small typed protocol, and turns the backend transforms into thin Datum / DataProto adapters over a common MergedSegment.

What changed

New module rllm/experimental/common/step_merge.py:

  • MergedSegment — post-merge intermediate (prompt_ids, response_ids, plus per-flat-token response_mask / response_logprobs / response_advantages and an extras dict).
  • merge_trajectory_steps(trajectory, *, token_ops, ...) — walks Trajectory.steps once and emits one MergedSegment per cumulative-prefix run.
  • TokenOps Protocol typed against TokenInput from rllm/experimental/rollout/types.py. DefaultTokenOps covers list[int] backends (Verl) and is the default; Tinker ships its own _TinkerTokenOps that handles EncodedTextChunk unwrapping and chunk .length counting.
  • PerTokenExtras (Tinker router_replay) and per_segment_extras (Verl multi_modal_inputs) are the two extension hooks the existing backends needed.

Backend transforms become thin adapters:

  • rllm/trainer/tinker/transform.py: trajectory_to_datums calls the merge with _TINKER_OPS, then _segment_to_datum builds the Tinker Datum (concat prompt+response, prepend prompt-region zeros, right-shift).
  • rllm/experimental/verl/transform.py: _process_trajectory calls the merge with the default ops, pad_short_logprobs=True, skip_steps_without_model_output=True, then materialises tensors and feeds accumulated.add_step.

Net diff in existing files: -262 / +136 lines (~−40%).

Tests

New tests/unified_trainer/test_step_merge.py (33 tests) covers the merge directly with both the default int ops and a mock chunk-aware TokenOps mirroring what Tinker ships:

  • Single-step + per-token advantage
  • Multi-step prefix merging (int and chunks)
  • Prefix breaks and mixed prefix/break
  • ImageChunk equality on prefix matches and splits; delta_obs containing chunks expands the per-token arrays correctly
  • Strict (require_logprobs / require_advantage) vs tolerant (pad_short_logprobs, missing→zeros) modes
  • skip_steps_without_model_output filter
  • PerTokenExtras (routing matrices) with pad behavior over delta_obs and steps lacking the field
  • per_segment_extras (multimodal) taken from each segment's first step
  • Per-flat-token length invariant in both ops

The Tinker test file drops TestIsPrefix and TestFlattenTokenInput (now subsumed by test_step_merge.py) and keeps the Datum-builder tests; one assertion-message regex was relaxed to match the new wording.

Test plan

  • ./venv/bin/python -m pytest tests/unified_trainer/ — 70 passed (33 step_merge + 9 tinker + 6 verl + 22 others)
  • No regressions in broader tests/ suite (the same 13 pre-existing failures on main reproduce, unrelated to this change)

🤖 Generated with Claude Code

Both the Tinker and Verl transforms used to implement near-identical
cumulative-prefix step merging in backend-specific code, with the same
loop accessing slightly different fields. That made debugging or evolving
the merge in one backend require touching the other, and duplicated the
delta-extension logic.

This change pulls the merge into rllm/experimental/common/step_merge.py
behind a small typed protocol:

  - MergedSegment is the post-merge intermediate (prompt_ids, response_ids,
    plus per-flat-token mask/logprobs/advantages and an extras dict).
  - merge_trajectory_steps() walks Trajectory.steps once and emits one
    MergedSegment per cumulative-prefix run.
  - TokenOps is a Protocol; DefaultTokenOps covers list[int] backends
    (Verl) and lives in step_merge.py. Tinker ships its own
    _TinkerTokenOps that handles EncodedTextChunk unwrapping and chunk
    .length counting (typed against TinkerTokenInput).
  - PerTokenExtras (router_replay) and per_segment_extras (multimodal
    inputs) are the two extension hooks the existing backends needed.

Backend transforms are now thin Datum/DataProto adapters over
MergedSegment. Net diff in the existing files: -262 / +136 lines.

Tests: new tests/unified_trainer/test_step_merge.py covers both the
default int ops and a mock chunk-aware ops (single step, prefix merging,
prefix breaks, image-chunk equality, strict vs tolerant, per-token and
per-segment extras, length invariant). The Tinker test file drops the
TestIsPrefix and TestFlattenTokenInput classes (now subsumed) and keeps
the Datum-builder tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@listar2000 listar2000 changed the title refactor: extract step merging into a shared backend-agnostic module refactor(unified_trainer): extract step merging into a shared backend-agnostic module May 10, 2026
@listar2000 listar2000 marked this pull request as ready for review May 12, 2026 03:57

@claude claude 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.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, reopen this pull request to trigger a review.

@luyuzhe111

Copy link
Copy Markdown
Collaborator

would like to see some e2e tests on this one to ensure no regression : )

@listar2000

Copy link
Copy Markdown
Collaborator Author

would like to see some e2e tests on this one to ensure no regression : )

For sure. Would do the tests soon.

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