Skip to content

Add mixed-precision (FP32) LM output logits - #6252

Merged
deepakn94 merged 12 commits into
NVIDIA:mainfrom
mkhona-nvidia:feat/fp32-lm-output-logits
Aug 12, 2026
Merged

Add mixed-precision (FP32) LM output logits#6252
deepakn94 merged 12 commits into
NVIDIA:mainfrom
mkhona-nvidia:feat/fp32-lm-output-logits

Conversation

@mkhona-nvidia

@mkhona-nvidia mkhona-nvidia commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
  • I, the PR author, have personally reviewed every line of this PR.

What does this PR do?

Allow the language model to compute output_layer with logits dtype leaving the accumulator in FP32, rather than casting down to BF16 as default. We use TE's general_gemm API to do this and modify autograd to cast the gradients back down to BF16, following Megatron-LM's contract with TE for GEMMs.

The legacy path performs FP32 accumulation internally but rounds every vocabulary logit to BF16 before cross entropy. Casting those rounded values back to FP32 cannot recover the discarded bits. Requesting an FP32 GEMM output preserves the accumulator result and materially reduces error at every tested reduction width.

The explicit backward cast does not introduce a new gradient contract: it reproduces the cast that autograd already performs through the legacy post-GEMM logits.float() operation.

Tests

  • FP32 output and backward contract: verifies BF16 input/weight GEMMs produce FP32 logits while input and weight gradients remain BF16 and match the legacy path.
  • Integer bitwise exactness: uses integer-valued BF16 operands in [-8, 8] with K=512. Since K * 8^2 = 32,768 < 2^24, every product and partial sum is exactly representable in FP32; the test compares raw FP32 words against a PyTorch FP32 reference with no tolerance.
  • Plain Transformer Engine parity: compares the complete Megatron wrapped linear path against a direct transformer_engine.pytorch.cpp_extensions.general_gemm call configured with the same FP32 output contract, requiring bitwise-identical outputs.
  • Backward regression sweep: applies the same FP32 upstream gradient to legacy and mixed-output paths across hidden sizes 512–16,384; BF16 input and weight gradients are bitwise identical at every width (Slurm job 2825327).
  • Numerical accuracy sweep: across hidden sizes 512–16,384, FP32-output logit RMSE improves by 181.2×–3,323.6× and cross-entropy absolute error is lower at every tested width relative to legacy BF16 logits (Slurm job 2825327).
  • Kernel benchmark: compares preallocated BF16-output torch.matmul against Transformer Engine general_gemm with an FP32 output over 15 shapes (M in 16, 128, 1,024; K in 1,024–16,384; N=32,768). The median overhead was 5.4% overall, 2.2% for K >= 4,096, and 1.9% for K >= 8,192; 11/15 shapes were within 10% and the full range was 0.3%–15.9% (NVIDIA GB300, Slurm job 2825411).
  • CLI contract: verifies --output-logit-dtype defaults to bf16, accepts only bf16 and fp32, and rejects fp16.

GEMM kernel benchmark

Tokens (M) Hidden (K) BF16 output FP32 output Overhead
16 1,024 0.022 ms 0.025 ms +15.8%
16 2,048 0.025 ms 0.026 ms +5.4%
16 4,096 0.046 ms 0.046 ms +1.5%
16 8,192 0.086 ms 0.087 ms +1.0%
16 16,384 0.193 ms 0.194 ms +0.3%
128 1,024 0.022 ms 0.026 ms +15.9%
128 2,048 0.030 ms 0.033 ms +10.3%
128 4,096 0.053 ms 0.057 ms +6.6%
128 8,192 0.104 ms 0.107 ms +2.9%
128 16,384 0.192 ms 0.195 ms +1.6%
1,024 1,024 0.054 ms 0.062 ms +14.9%
1,024 2,048 0.087 ms 0.093 ms +7.2%
1,024 4,096 0.158 ms 0.168 ms +6.2%
1,024 8,192 0.309 ms 0.317 ms +2.5%
1,024 16,384 0.659 ms 0.674 ms +2.2%

The benchmark uses preallocated output buffers, 10 warmup pairs, 30 alternating-order CUDA-event samples per kernel, and reports medians. The vocabulary dimension is 32,768 for every shape. The larger-width results show that preserving FP32 GEMM output has low kernel-level overhead for representative LM heads.

Hidden size Legacy logit RMSE FP32-output RMSE RMSE improvement Legacy CE abs. error FP32-output CE abs. error Backward grads
512 1.690e-03 5.084e-07 3323.6× 2.804e-04 0.000e+00 Exact
1,024 1.674e-03 1.028e-06 1628.6× 4.301e-04 9.537e-07 Exact
2,048 1.674e-03 2.156e-06 776.5× 2.527e-04 1.907e-06 Exact
4,096 1.664e-03 4.464e-06 372.9× 4.387e-05 2.861e-06 Exact
8,192 1.660e-03 9.161e-06 181.2× 8.106e-05 5.722e-06 Exact
16,384 1.663e-03 2.862e-06 581.0× 4.501e-04 9.537e-07 Exact

Integer bitwise exactness results

Hidden size FP32-output bit mismatches Legacy value mismatches Result
512 0 / 16,384 6,767 / 16,384 Exact
1,024 0 / 16,384 8,557 / 16,384 Exact
2,048 0 / 16,384 10,027 / 16,384 Exact
4,096 0 / 16,384 11,335 / 16,384 Exact
8,192 0 / 16,384 12,443 / 16,384 Exact
16,384 0 / 16,384 13,311 / 16,384 Exact

Both operands are BF16 tensors containing integers in [-8, 8]. For every width, K * 8^2 < 2^24, so the products and possible partial sums are exactly representable in FP32. The raw FP32 output words matched the FP32 PyTorch reference with no tolerance at every width (NVIDIA GB300).

Method

  • Inputs and output-layer weights are BF16; the reference uses their exactly represented FP32 values.
  • Legacy path: PyTorch BF16 output-layer GEMM, followed by the existing FP32 logits cast.
  • New path: Transformer Engine general_gemm with out_dtype=torch.float32.
  • Reference: PyTorch FP32 linear projection with TF32 disabled.
  • Shape sweep: 16 x K activations, 32,768 x K vocabulary weights, K in 512, 1024, 2048, 4096, 8192, 16384.
  • Hardware: NVIDIA GB300; seed 1234; commit `27ef2b465.

Issue tracking

For PRs from open-source community contributors:

  • New features: a linked issue is required. Please open a feature request and reference it here before submitting the PR.
  • Small updates (bug fixes, minor improvements): a linked issue is recommended and will accelerate the PR review process.

Linked issue:

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code Typing guidelines
  • I have added relevant documentation
  • I have run the autoformatter.sh on my PR

Code review

Feel free to message or comment @NVIDIA/mcore-oncall to help accelerate your merge into main. The less complex your PR is, the faster it will be approved and merged!

All PRs start as draft. If you open a non-draft PR, it will be automatically converted to draft.

Step 1: Mark PR as "Ready for Review"

  1. When your PR is ready, click Ready for Review.
  2. An oncall reviewer is auto-assigned and expert reviewers are notified based on your changes.
    • Some PRs may jump straight to step 2. This is determined by .github/CODEOWNERS.

⚠️ Only mark as ready once merge-conflicts are resolved and the CI is passing.
Final Review might get declined if these requirements are not fulfilled.

Step 2: Final Review

For PRs that change megatron/core, once all expert reviewers have approved, the Final Review label is applied automatically and final reviewers are assigned.

For PRs outside megatron/core, this step is skipped.

Step 3: Approved

Once all required reviewers have approved, the Approved label is applied automatically.

Merge

Any member of mcore-engineers will be able to merge your PR.

Signed-off-by: mkhona <mkhona@nvidia.com>
Signed-off-by: mkhona <mkhona@nvidia.com>
Signed-off-by: mkhona <mkhona@nvidia.com>
@mkhona-nvidia
mkhona-nvidia requested a review from deepakn94 August 4, 2026 18:42
@mkhona-nvidia
mkhona-nvidia requested review from a team as code owners August 4, 2026 18:42
@copy-pr-bot

copy-pr-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

This PR has been automatically converted to draft because all PRs must start as drafts.

When you are ready for review, click Ready for Review to begin the review process. This will:

  1. Add the oncall reviewer (optional reviewer)
  2. Add required review teams based on your changes

See the contribution guide for more details.

@mkhona-nvidia
mkhona-nvidia requested a review from skyw August 4, 2026 19:43
@skyw
skyw removed their request for review August 4, 2026 19:47
@deepakn94

Copy link
Copy Markdown
Contributor

/claude review

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Light review — one substantive concern, otherwise LGTM.

Default --output-logit-dtype=bf16 is not behavior-preserving for non-bf16 training runs.

validate_args unconditionally maps the default to torch.bfloat16 (arguments.py line 1055): args.logit_dtype = map_dtype(getattr(args, "logit_dtype", None)), where bf16 maps to torch.bfloat16.

The fast path in _linear_forward only bypasses the TE general_gemm route when output_dtype == input.dtype. That holds for the common bf16 run (input is bf16, so no change). But:

  • --fp16 runs: params_dtype = torch.half, so the output-layer input is fp16. bf16 != fp16, so logits are now forced to bf16 via TE, where the legacy path produced fp16. This is a silent dtype/precision change for every existing fp16 recipe, and it now requires TE general_gemm to be present (otherwise the new RuntimeError fires on a previously-working config).
  • fp32 runs: input is fp32, so logits get downcast to bf16 — a precision regression vs. legacy fp32 logits.

Since the stated goal is opt-in FP32 logits, a dtype-neutral default (e.g. None/auto, or gating the value against params_dtype) would keep the fast path active and preserve legacy behavior unless the user explicitly asks for a different logit dtype. As written, the always-on bf16 default assumes bf16 training.

Could you confirm the fp16/fp32 training paths were considered, or add a guard so the default does not alter those runs?

@deepakn94
deepakn94 marked this pull request as ready for review August 9, 2026 21:32
Signed-off-by: mkhona <mkhona@nvidia.com>
@deepakn94

Copy link
Copy Markdown
Contributor

/ok to test 2800628

mkhona-nvidia and others added 3 commits August 10, 2026 13:52
Signed-off-by: Keshav Santhanam <ksanthanam@nvidia.com>
Explicitly set inference logits dtype
@mkhona-nvidia
mkhona-nvidia requested review from a team as code owners August 10, 2026 21:19
@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the Approved All necessary approvals have been made label Aug 10, 2026

# Explicitly cast logits to expected dtype
logits = logits.to(self.config.params_dtype)
logits = logits.to(self.logit_dtype)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would this be a no-op with fp32 logit dtype?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the Approved All necessary approvals have been made label Aug 11, 2026
This reverts commit a8499c1, reversing
changes made to 81e9534.

Signed-off-by: mkhona <mkhona@nvidia.com>
@deepakn94

Copy link
Copy Markdown
Contributor

/ok to test c2ca628

@svcnvidia-nemo-ci

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started!

You can track the progress here: https://github.com/NVIDIA/Megatron-LM/actions/runs/31563547355

Merged via the queue into NVIDIA:main with commit 897649d Aug 12, 2026
184 of 185 checks passed
@deepakn94
deepakn94 deleted the feat/fp32-lm-output-logits branch August 12, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.