Repository navigation
[model] fix: clamp the first CP chunk slice in preprocess_packed_seqs - #5614
Conversation
Chunk indices are derived from the alignment-padded length while `d` holds only the valid tokens. When a row's valid length is shorter than the padding, the first-chunk slice can start past the end of `d` and produce an empty tensor, so the assignment raises: RuntimeError: The expanded size of the tensor (2) must match the existing size (0) at non-singleton dimension 0. Target sizes: [2]. Tensor sizes: [0] The remaining chunk right below is already clamped with `min(remain_end, d.shape[0])`; this applies the same guard to the first chunk and adds the matching `max(..., 0)` to `remain_len`. The same defect is vendored in two copies (qwen3_vl and exaone45), both fixed here. verl fixed the identical code in verl-project/verl#6001. Rows long enough to be unaffected produce bit-identical output, verified with torch.equal for valid lengths 128 / 4096 / 14000 at both CP ranks. Co-authored-by: Claude Signed-off-by: gaohongkui <gaohongkui1021@163.com>
|
/ok to test 00e11d1 |
|
/ok to test 00e11d1 |
|
Thanks for the review and the The three red checks are a transient GitHub outage, not this change. All three failing Jobs 95455789649 / 95455789674 / 95455791179, all at 17:58:5x UTC. Because Evidence that a re-run is all this needs: the fourth workflow (Build/validate/release, run 32052731736) has already been re-run as attempt 2, and its Could someone re-run these three?
I've deliberately not pushed anything or used "Update branch", since a new SHA would invalidate the approval and both |
Problem
In
preprocess_packed_seqs, whencp_size > 1, the chunk indices are computed in thealignment-padded coordinate space (
seqlen_padded_i, aligned totp_size * cp_size * 2),while
d = input_ids[i, attention_mask[i]]holds only the valid tokens.When a row's valid length is shorter than the alignment padding, the first-chunk slice
d[half_seqlen * cp_rank : half_seqlen * (cp_rank + 1)]can start past the end ofdandyield an empty tensor, so the assignment raises:
The remaining chunk immediately below is already clamped with
min(remain_end, d.shape[0])— only the first chunk was missed. The identical code in verlwas fixed in verl-project/verl#6001.
The same function is vendored in two places here, both with the same gap; both are fixed:
src/megatron/bridge/models/qwen_vl/modelling_qwen3_vl/utils.pysrc/megatron/bridge/models/exaone/exaone45/modelling_exaone45/utils.pyMeasured thresholds for the unclamped path (largest valid length that still crashes):
align_sizeLarger TP×CP grids are affected at larger valid lengths, so short prompts plus a wide
parallel grid is the general trigger.
How we hit it
Qwen3.6-35B-A3B RL training on H20-141G, THD (
use_remove_padding) + TP2/CP2, verl drivingMegatron through the bridge. Our trigger is verl's multi-trajectory divisibility padding row
— a deliberate 2-token no-op sample — but nothing about the crash is specific to that source.
The runtime module inspection was unambiguous: verl's own copy of the function carries the
clamp, both vendored copies here do not, and the traceback lands in the vendored copy.
Causality was established by single-variable isolation: with a longer padding row the same
config trains fine for 4 steps; reverting only the row length reproduces the crash.
Fix
Clamp the first chunk exactly as the remaining chunk already is, and add the matching
max(..., 0)toremain_len. Plus a regression test.Test
pre-commit run --files <the three changed files>— all hooks pass (ruff, ruff-format,trailing whitespace, end-of-file).
New unit test
test_preprocess_packed_seqs_row_shorter_than_alignmentintests/unit_tests/models/qwen_vl/modelling_qwen3_vl/test_utils.py, following the existingtest_preprocess_packed_seqs(same 2-GPUskipif, same_setup_parallel_statehelper): abatch with one full-length row and one single-token row. With tp=1/cp=2 the alignment is 4,
so the single-token row is the smallest case that reaches the out-of-range slice on
cp_rank 1. It raises before this change and passes after.
Behaviour of the chunk assignment, reproduced for both variants at TP=CP=2, both CP ranks:
Rows long enough to be unaffected are bit-identical, i.e. this is a pure guard:
End to end on the cluster: with the crash avoided, THD + CP=2 completes 4 training steps and
reward magnitude matches an otherwise identical CP=1 run (0.797/0.697/0.473/0.472 vs
0.847/0.415/0.483/0.457 — GRPO rollout is sampled, so only magnitude and trend are
comparable). Peak reserved memory 99.2 GB (CP=2) vs 113.9 GB (CP=1) of 141 GB.
Scope note: clamping prevents the crash; it does not make a very short row semantically
meaningful (some CP ranks end up holding only zeros for it). That matches the behaviour of
the already-clamped remaining chunk, and is harmless for the padding-row case that triggered
it here (no gradient, excluded from metrics).
Not a duplicate
Searched this repo for
preprocess_packed_seqs,context parallel packed, andexpanded size(PRs, all states). The closest open PR, #3869 ("Stabilize Qwen3-VL packedSFT"), does touch
modelling_qwen3_vl/utils.py, but its hunks are at lines ~200/230/636 anddo not touch the CP chunk assignment — no overlap and no conflict. #4304 is about adding
packed-sequence support for qwen3-omni. Also filed the same fix for the other vendored copy
of this function in ISEEKYAN/mbridge#157.
AI assistance
This change was prepared with AI assistance (Claude). I reviewed every changed line, ran the
checks above myself, and can defend the change end-to-end.