Repository navigation
perf(train): make sequence packing alignment-aware - #2370
dyurk-lila wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the sequence packing and alignment logic to separate per-sequence layout alignment constraints (for CP/SP) from aggregate token slab alignment constraints (for TP/FP8). Instead of padding every sub-sequence to the global alignment size, sub-sequences are padded to their individual layout alignment constraints, and the aggregate TP/FP8 alignment padding is applied once at the end of the packed token slab. Feedback on these changes suggests pre-aligning token counts to packed_length_multiple in TokenBasedBatchIterator when sub_seq_lengths is present to prevent underestimating aggregate padding, as well as adding defensive checks to ensure tensors are not empty before indexing their last elements in preprocess_packed_seqs and build_token_metadata_layout.
| sub_seq_lengths = data.get("sub_seq_lengths") | ||
| if sub_seq_lengths is None: | ||
| self._token_counts = data["attention_mask"].sum(dim=1).cpu().tolist() | ||
| else: | ||
| self._token_counts = [ | ||
| sum(length + (-length % sequence_length_multiple) for length in row.tolist()) for row in sub_seq_lengths | ||
| ] | ||
| sequence_length_multiple = 1 # The segment footprints are already aligned. |
There was a problem hiding this comment.
When sub_seq_lengths is not None, each row represents an already-packed bin with its own independent aggregate tail padding. Since these bins are processed as separate rows in the microbatch rather than concatenated, their padding overhead is paid independently. Pre-aligning self._token_counts to packed_length_multiple and setting packed_length_multiple = 1 prevents the packer from under-estimating the aggregate padding when grouping multiple bins, which could otherwise lead to over-packing and potential OOMs.
| sub_seq_lengths = data.get("sub_seq_lengths") | |
| if sub_seq_lengths is None: | |
| self._token_counts = data["attention_mask"].sum(dim=1).cpu().tolist() | |
| else: | |
| self._token_counts = [ | |
| sum(length + (-length % sequence_length_multiple) for length in row.tolist()) for row in sub_seq_lengths | |
| ] | |
| sequence_length_multiple = 1 # The segment footprints are already aligned. | |
| sub_seq_lengths = data.get("sub_seq_lengths") | |
| if sub_seq_lengths is None: | |
| self._token_counts = data["attention_mask"].sum(dim=1).cpu().tolist() | |
| else: | |
| self._token_counts = [ | |
| sum(length + (-length % sequence_length_multiple) for length in row.tolist()) for row in sub_seq_lengths | |
| ] | |
| self._token_counts = [ | |
| length + (-length % packed_length_multiple) for length in self._token_counts | |
| ] | |
| sequence_length_multiple = 1 # The segment footprints are already aligned. | |
| packed_length_multiple = 1 # The row footprints are already fully aligned. |
| # TP and FP8 operate on the aggregate local token slab. Attach their tail | ||
| # padding once to the final sequence rather than to every sequence. | ||
| aggregate_pad_size = (-seqlens_in_batch_padded.sum()) % packing_align_size_total | ||
| seqlens_in_batch_padded[-1] += aggregate_pad_size |
There was a problem hiding this comment.
Add a defensive check to ensure seqlens_in_batch_padded is not empty before attempting to index its last element, preventing potential runtime index errors on empty or degenerate batches.
| # TP and FP8 operate on the aggregate local token slab. Attach their tail | |
| # padding once to the final sequence rather than to every sequence. | |
| aggregate_pad_size = (-seqlens_in_batch_padded.sum()) % packing_align_size_total | |
| seqlens_in_batch_padded[-1] += aggregate_pad_size | |
| # TP and FP8 operate on the aggregate local token slab. Attach their tail | |
| # padding once to the final sequence rather than to every sequence. | |
| if seqlens_in_batch_padded.numel() > 0: | |
| aggregate_pad_size = (-seqlens_in_batch_padded.sum()) % packing_align_size_total | |
| seqlens_in_batch_padded[-1] += aggregate_pad_size |
| aggregate_pad_size = (-padded_sequence_lengths_tensor.sum()) % packing_align_size_total | ||
| padded_sequence_lengths_tensor[-1] += aggregate_pad_size |
There was a problem hiding this comment.
Add a defensive check to ensure padded_sequence_lengths_tensor is not empty before indexing its last element, preventing potential runtime index errors on empty or degenerate batches.
| aggregate_pad_size = (-padded_sequence_lengths_tensor.sum()) % packing_align_size_total | |
| padded_sequence_lengths_tensor[-1] += aggregate_pad_size | |
| if padded_sequence_lengths_tensor.numel() > 0: | |
| aggregate_pad_size = (-padded_sequence_lengths_tensor.sum()) % packing_align_size_total | |
| padded_sequence_lengths_tensor[-1] += aggregate_pad_size |
|
| `max_tokens_per_microbatch` (padding the bin count to a multiple of the | ||
| data-parallel size); each bin becomes one row of the dispatched batch and one |
There was a problem hiding this comment.
Capacity omits alignment minimum
This says bins have capacity max_tokens_per_microbatch, but the collator raises that capacity to the aggregate alignment when it is larger. For example, TP4 with blockwise FP8 can use a 512-token capacity even when the configured budget is lower. This makes memory and batch-shape planning misleading. The SFT README gives the same capacity description; both guides should explain the alignment-driven minimum.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| fp8_enabled = is_fp8_enabled(transformer_config_kwargs.get("fp8")) | ||
| packer = make_seq_packer( | ||
| PackingStrategy.MODIFIED_FIRST_FIT_DECREASING, | ||
| bin_capacity=trainer.sft_cfg.resolved_bin_capacity(), |
There was a problem hiding this comment.
Parity bins use different capacity
_recompute_flat_bins uses the configured budget without the collator's alignment-driven minimum, and its aggregate alignment calculation omits fp8_recipe. When FP8 alignment exceeds the budget, the collator can combine short sequences in a bin that this helper splits. The resulting example-to-bin mapping differs, so the parity comparison no longer checks the intended examples. Compute capacity and recipe the same way as the collator.
186ba55 to
0b85aa0
Compare
5ad242e to
8a1b45d
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8a1b45d. Configure here.
8a1b45d to
3866168
Compare
3866168 to
2e5107a
Compare
Signed-off-by: Dominic Yurk <dyurk@lila.ai>
Signed-off-by: Dominic Yurk <dyurk@lila.ai>
…budget Signed-off-by: Dominic Yurk <dyurk@lila.ai>
Signed-off-by: Dominic Yurk <dyurk@lila.ai>
Signed-off-by: Dominic Yurk <dyurk@lila.ai>
2e5107a to
7e2d303
Compare

What does this PR do?
Account for the physical aligned footprint of packed sequences, applying CP layout alignment per sequence and TP/FP8 tail padding once per packed slab.
TLDR: packing capacity checks, collation, Megatron preprocessing, replay metadata, and token-based batching use the same alignment model.
How it works
sequence_length_multipleaccounts for constraints on each sequence, including CP/SP layout gaps.packed_length_multipleaccounts for TP/FP8 alignment after sequence footprints are combined.Charging TP/FP8 alignment once per packed slab avoids padding every short sequence to a constraint that applies only after packing. Per-sequence CP constraints still apply before placement, preventing an apparently valid bin from overflowing after layout padding.
Relationship to the general FP8 training PR
#1898 adds broad end-to-end FP8 training and rollout support. Its packing changes add FP8-recipe-specific tile requirements and make SFT bin packing account for aligned lengths, but combine CP/SP, TP, and FP8 into one alignment paid independently by every packed sequence.
This PR addresses a different scope question: CP/SP layout gaps are genuinely per-sequence, while TP and FP8 constrain the aggregate local token slab and should be paid once per packed bin or microbatch. It also moves this two-level accounting into the generic packers, covering non-FP8 CP/TP layouts and worker-side balanced token batching in addition to controller-side FP8 SFT packing.
The changes are complementary rather than replacements. When the branches are reconciled, #1898’s recipe-specific blockwise/MXFP8 tile requirement should feed
packing_align_size_total, while this PR’spacking_align_size_sequenceversuspacking_align_size_totalsplit determines where that padding is charged.Validation
At the current series tip, focused CPU suites passed in both mirrors: 415 tests across collation, DP sampling, worker batching, layout, active spans, and Megatron correctness (13 Megatron-only skips); 40 bin-packer tests; and 3 unpacked active-span guard tests with megatron-core installed. Ruff and Black passed. Seven targeted Triton value/gradient tests passed on one H100; the full GPU suite and benchmarks were not rerun.
Review order
Part 2 of 9 in a linked series. The stack is based on
main; each later PR targets the preceding layer. Review and merge from bottom to top.Depends on #2369. Review only this layer.
Note
High Risk
Changes packed THD layout, bin budgets, and loss normalization across Megatron CP/TP/FP8; misalignment would corrupt gradients silently, though tests cover collation, preprocess, and batching parity.
Overview
Makes Megatron sequence packing alignment-aware by splitting padding into per-sequence layout gaps (CP/SP) versus a single aggregate tail pad (TP/FP8), and threading that model through collation, bin packing, preprocessing, and worker microbatching.
Replaces
get_packed_seq_align_sizewithget_packing_align_size_sequenceandget_packing_align_size_total. Generic packers (SeqPacker, FFD/MFFD/balanced) gainsequence_length_multipleandpacked_length_multipleso capacity checks use physical footprint; MFFD can place an aligned singleton alone when it exceeds the token budget.PackedDataCollatorbins onmax_tokens_per_microbatchwith those multiples;preprocess_packed_seqs, token metadata, valid masks, and per-token loss counts mirror the same segment offsets and one-shot aggregate padding.TokenBasedBatchIteratorcountssub_seq_lengthsalignment gaps and passes the same multiples fromMegatronWorker.Docs now describe controller MFFD bin capacity as
max_tokens_per_microbatch(must be>= max_length) instead of implying per-sequencemax_lengthbins or strict multiples ofmax_length.Reviewed by Cursor Bugbot for commit 7e2d303. Bugbot is set up for automated code reviews on this repo. Configure here.