Repository navigation
fix(rl): reduce Megatron peak memory in selective_log_softmax, causal mask, and discarded logits - #322
Open
Wings-Of-Disaster wants to merge 3 commits into
Open
fix(rl): reduce Megatron peak memory in selective_log_softmax, causal mask, and discarded logits#322Wings-Of-Disaster wants to merge 3 commits into
Wings-Of-Disaster wants to merge 3 commits into
Conversation
…nning it The per-row loop was documented as 'loop to reduce peak mem consumption', but under autograd every row's full-vocab intermediate is saved for backward, so the loop pins one copy of the vocabulary per row (two when return_entropy is set) until the backward pass finishes. At 16k tokens and a 151936-word vocab that is 4.6 GiB of extra resident activation per micro-batch in bfloat16, plus tens of thousands of tiny allocations and kernel launches. Replace the loop with a chunked autograd.Function that computes in float32 and saves only the original-dtype chunk and its labels, recomputing the softmax in backward. Nothing wider than the labels is retained, and the float32 compute also removes the bfloat16 quantization of the log-probabilities (~0.2 ULP at the magnitudes a peaked row produces).
…builds Two defects combined to make drop_causal_4d_mask dead code: - MegatronModel.forward_backward read attention_mask_type off the model config, but neither mcore TransformerConfig nor mcore_bridge ModelConfig declares that attribute, so the processor always received None and its 'attention_mask_type != causal' guard always returned early. - Even when it did fire, the hook runs four pipeline steps after collate_fn, so the [B, 1, S, S] bool mask had already been allocated (256 MiB per micro-batch at S=16384, roughly twice that in peak with the tril/invert temporaries) only to be set to None. Default the type to 'causal' for decoder-only causal_lm and decide in collate_fn itself, so _create_4d_attention_mask is never called on the NPU/MindSpeed path. The hook stays as a fallback for callers that build the mask elsewhere.
reduce_loss() unconditionally puts logits.detach() into the per-microbatch report dict and forward_backward appends every one of them, so each micro-batch's full-vocab logits stay resident until the pass ends -- and `if not return_logits: logits = None` drops them one line after the cat. At 16k tokens per micro-batch, two micro-batches and a 151936-word vocab that is 4.6 GiB of bfloat16 logits per micro-batch, so 9.3 GiB held across the backward pass where memory is tightest. When variable_seq_lengths is False the cat adds a second full-size buffer on top and the peak reaches 18.6 GiB; it defaults to True, so 9.3 GiB is the usual figure. Report the logits only when the caller asked for them, mirroring the guard TransformersModel.forward has had since 6921342. Losses that do want logits are unaffected: they read output_tensor directly from the enclosing scope rather than the report dict. Also skip None entries when collecting the report dicts. reduce_loss writes the key unconditionally, with a None value when there is nothing to report, so `'logits' in loss_dict` was true for a micro-batch that had none and the list reached torch.cat with a None in it. The 'logps' half matters independently: reduce_loss reports None for it when a microbatch's data processing failed under TWINKLE_FAIL_FAST=0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR type
PR information
Fix three sources of unnecessary memory use in the Megatron loss and evaluation path. Each fix is a separate commit.
Base:
origin/mainat96dab686.selective_log_softmaxcausal_lmto causal attention and skip mask creation incollate_fnwhen the backend supplies it.return_logits=False. Fixed-length batches also concatenate them before discarding them.Implementation and compatibility
Nonevalues for both logits and log-probabilities. This also handles failed micro-batches underTWINKLE_FAIL_FAST=0without passingNonetotorch.cat.return_logits=True, and the tensor-parallel early return are unchanged. Loss functions still access logits directly.Mask correctness
For right-padded sequences, the explicit mask and the backend's causal mask differ only at padded query positions. Real tokens cannot attend to later padding, and padded positions are excluded from the loss by
labels = -100. Padded outputs must remain finite: multiplying a NaN by a zero loss mask would still produce a NaN.The reported device check found a maximum output difference of
0.0at real positions and1.50at padded positions. The inspected FlashAttention path uses the causal flag and does not read the explicit mask.Experiment results
Environment: PyTorch 2.10.0, Transformer Engine 2.9.0, FlashAttention 2.8.2. Vocabulary size: 151,936.
Memory
Saved tensor storage was measured with
saved_tensors_hooks, deduplicated by storage pointer, and excludes the input storage. Measurements used 256 rows; the figures below are extrapolated to 16,384 rows. These are retained-storage estimates, not measured accelerator peak memory.Float32 without entropy already retained no extra full-vocabulary storage; its benefit is fewer save calls. Input tensors, gradients, and temporary buffers are not included in the zero figures above.
Two additional allocation estimates:
variable_seq_lengths=False. The fix avoids these reporting allocations.Numerical accuracy and validation
3.4e-02to7.4e-03.gradcheckpassed with and without entropy. Direct gradient comparisons againstlog_softmax → gathergave maximum differences of8.941e-08and2.235e-08, respectively.test_fallback_*failures intest_liger_fused_linear_ce.pyoccur on unmodified main.pre-commit run --all-files: all hooks passed.Example: why the row-by-row loop retains memory
The previous bfloat16 path computed a full-vocabulary result for each row, then selected only the target token's log-probability:
Selecting one value does not release the rest of the row:
log_softmaxsaves its output for backward. At 16,384 rows, all 16,384 outputs remain in memory, equivalent to another full logits tensor:The replacement recomputes softmax in chunks during backward instead of retaining those outputs.
Example: where the two causal masks differ
Consider right-padded sequences with lengths
[4, 2]and a padded length of4. For the second sequence, positions0–1are real tokens and2–3are padding.With
Truemeaning blocked, the explicit mask isT[r,c] = (c > r) OR (c >= 2). The backend's causal mask isC[r,c] = (c > r).Tadditionally blocks column 2Tadditionally blocks columns 2 and 3The masks are not identical, but real-token outputs are unchanged: each query uses its own mask row, and causal attention prevents real tokens from reading later padding in subsequent layers. Padded positions have
labels = -100and are excluded from the loss.The finiteness condition still matters. For this example, both masks leave at least one accessible key in every row, avoiding fully masked rows. Loss masking alone would not protect against NaNs because
0 × NaN = NaN.Example: why the computation and output use float32
The original numerical checks illustrate two different limits:
-inf-11.938-11.9311.25e-022.00e-013.05e-06For a flat row,
sum(exp(x - max(x))) = 151,936, exceeding float16's maximum finite value of65,504if the sum is stored in float16. The overflow result above describes the tested computation, not every softmax implementation.Bfloat16 has enough range, but adjacent representable values near −100 are
0.5apart. Casting the result back to bfloat16 would lose the precision gained during computation. This is why bfloat16 inputs now return float32 log-probabilities and entropy.