Skip to content

fix: MTP CP-aware roll, segment-aware roll, and synthetic packing support - #4623

Open
chiaotung97 wants to merge 1 commit into
AI-Hypercomputer:mainfrom
antgroup:feature_mtp_cp_packing_fix
Open

fix: MTP CP-aware roll, segment-aware roll, and synthetic packing support#4623
chiaotung97 wants to merge 1 commit into
AI-Hypercomputer:mainfrom
antgroup:feature_mtp_cp_packing_fix

Conversation

@chiaotung97

Copy link
Copy Markdown

Summary

Fixes 3 issues when combining Multi-Token Prediction (MTP) with
All-Gather Context Parallelism (AG-CP) and Packing.

Checklist

  • CP-aware left shift (_shift_left_one_cp_aware) with ppermute backward ring
  • Segment-aware roll (roll_and_mask_by_segment) to prevent cross-document leakage
  • Synthetic data packed segment IDs (_make_packed_segment_ids)
  • Unit tests: 30/30 passing on TPU v6e-4 (4 devices)
  • Backward compatibility: all paths degrade gracefully
  • Merge conflicts with main resolved

Problem

# Issue Root Cause Impact
1 jnp.roll does not cross CP rank boundaries MTP shifts left each iteration to predict the "next token", but jnp.roll only operates within the local shard Cross-rank right-neighbor tokens are lost under CP
2 roll_and_mask is segment-unaware Under packing, a left shift can cross document boundaries (e.g., DocA's last token shifts into DocB's first) Cross-document target leakage contaminates gradient signals
3 Synthetic data has no real segment boundaries segment_ids is always all-ones even though base.yml defaults to packing: true Segment boundary logic is untestable on synthetic data; train_utils.py also blocks the combination outright

Solution

1. CP-Aware Left Shift (_shift_left_one_cp_aware)

Uses jax.lax.ppermute in a backward ring: rank r sends its first token to
rank r−1, receives rank r+1's first token, and places it in its last
position. Degrades to jnp.roll when no CP axis is in scope — zero overhead.

2. Segment-Aware Roll (roll_and_mask_by_segment)

Shifts both x and segment_ids via _shift_left_one_cp_aware, then masks
positions where seg_current != seg_next (document boundary) or
seg_current == 0 (padding). Falls back to roll_and_mask when
segment_ids=None. All rolling variables in
MultiTokenPredictionBlock.__call__ now use this function.

3. Synthetic Data with Packed Segment IDs

_make_packed_segment_ids splits each row into 2..N randomly-sized segments
with sequential integer IDs starting from 1. Removed the train_utils.py
guard that rejected synthetic + packing + CP.

Files Changed

File Change +/−
layers/multi_token_prediction.py _shift_left_one_cp_aware, roll_and_mask_by_segment, wiring +103/−4
input_pipeline/synthetic_data_processing.py _make_packed_segment_ids +43/−2
utils/train_utils.py Remove synthetic+packing+CP guard −5
tests/unit/multi_token_prediction_test.py 17 new tests (segment + CP + packed_ids) +296
docs/design/ag_cp_mtp_packing_fix.md Design document +141

Unit Tests

30 tests, all passing (TPU v6e-4, 4 devices):

Test class Tests Description
TestRollAndMask 3 Existing — local roll and mask
TestRollAndMaskBySegment 8 Segment boundary masking (no CP)
TestMakePackedSegmentIds 6 Segment ID generation: shape, contiguity, determinism
TestShiftLeftOneCpAware 5 CP-aware shift: 1D/2D/3D, last-rank zero, rank-boundary non-zero
TestRollAndMaskBySegmentWithCp 4 CP + segment boundaries, padding mask, single-seg, 3D
Upstream tests (layer, block, MTP loss, quantize) 4 Existing — unchanged

Backward Compatibility

  • _shift_left_one_cp_aware: degrades to jnp.roll when CP=1 or no "context" axis
  • roll_and_mask_by_segment: degrades to roll_and_mask when segment_ids=None
  • roll_and_mask(shift=-1): equivalent to original path when CP is off
  • synthetic_data_processing: segment IDs remain jnp.ones when packing=False

Verification

# Configuration TPU v6e-4
1 MTP + packing
2 MTP + CP=2
3 MTP + CP=2 + packing
4 Unit tests ✅ 30/30

- Add CP-aware left shift (_shift_left_one_cp_aware) using ppermute
- Add segment-aware roll (roll_and_mask_by_segment) to prevent
  cross-document target leakage in MTP
- Generate packed segment IDs for synthetic data when packing=True
- Remove guard against synthetic+packing+CP in train_utils.py
- Add 17 unit tests covering segment roll, CP shift, and CP+segment
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/maxtext/layers/multi_token_prediction.py 91.66% 1 Missing and 2 partials ⚠️
...axtext/input_pipeline/synthetic_data_processing.py 89.47% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@RissyRan

Copy link
Copy Markdown
Collaborator

Hi reviewers, just an FYI that this PR is from a forked repo. Due to secret key limitations, the Gemini review couldn't be triggered.

local_rolled = local_rolled.at[:, -1:, ...].set(0)

try:
cp_size = jax.lax.psum(1, axis_name=axis_name)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a very interesting way to collect cp_size. I suggest using

jax.lax.axis_size(axis_name)
OR
mesh.shape.get(axis_name, 1)

@NuojCheng NuojCheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM and thank you for the contribution! Some minor comments:

  • the cpu test failure can be solved by rebasing to main
  • I don't think we need design doc included in this PR, but it is great for PR review. Can we move the design doc to the PR description?

Returns:
Array shaped like x, left-shifted by 1 across CP boundaries.
"""
local_rolled = jnp.roll(x, -1, axis=1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would this still work with load balancing on? If not perhaps we should handle it differently or reject LB?

mtp_xent, _ = max_utils.cross_entropy_with_logits(
mtp_logits, jax.nn.one_hot(rolled_target_ids, cfg.vocab_size), 0.0
)
mtp_xent_masked = mtp_xent * rolled_target_mask

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is rolled_target_mask a boolean mask?

rolled_target_mask = roll_and_mask(rolled_target_mask)
rolled_position_id = roll_and_mask(rolled_position_id)
rolled_input_ids = roll_and_mask_by_segment(rolled_input_ids, rolled_segment_ids)
rolled_target_ids = roll_and_mask_by_segment(rolled_target_ids, rolled_segment_ids)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is rolled_segment_ids aligned with rolled_target_ids here? target_ids is already shifted by 1 so after this roll the actual target is 2 tokens ahead? segment IDs passed to the helper seem to track only 1 token

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants