Skip to content

Support Muon in BF16_Optimizer with FP32 gradient accumulation - #8526

Merged
delock merged 9 commits into
deepspeedai:masterfrom
0z5a:fix/muon-bf16-optimizer
Sep 18, 2026
Merged

delock merged 9 commits into
deepspeedai:masterfrom
0z5a:fix/muon-bf16-optimizer

Conversation

@0z5a

@0z5a 0z5a commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

BF16 weights with FP32 gradient accumulation and ZeRO stage 1 select BF16_Optimizer. Muon currently refuses that configuration because this wrapper hands the optimizer flat partitions without first applying Newton–Schulz. This change computes Muon updates on the original matrices after gradient reduction and global clipping, then copies only each rank's owned intersections into the optimizer update.

Momentum remains FP32 and partitioned like the master weights. The implementation gathers one group's momentum as temporary workspace, stages local momentum until the optimizer step, and excludes alignment padding. Auxiliary Adam groups retain their existing update path. The initial support boundary is dense data parallelism with two-dimensional Muon matrices; model-parallel MPU, expert groups and graph harvesting remain rejected. This adds communication and temporary memory proportional to the largest Muon group; it is a correctness change, with no performance claim.

Validation on two NVIDIA L20 GPUs (PyTorch 2.13.0+cu130):

Test Configuration / coverage Result
BF16 wrapper regressions Three pytest cases, each exercised at world sizes 1 and 2; standard/Gram NS, accumulation boundaries, partition-sized FP32 momentum, and the neighboring BF16-gradient configuration 3 passed
Eager engine E2E DP=2, accumulation over 3 microbatches, clipping=0.05, mixed Muon/Adam groups, and 17×13 / 9×17 matrices crossing partition boundaries; standard and Gram NS All 3 optimizer steps match the independent full-matrix reference exactly
Checkpoint continuation Save, continue for 2 steps, reload, and replay; standard and Gram NS Losses, weights, and optimizer state reproduce exactly
Final source validation Repeat the eager E2E using the final source files in an isolated directory Passed, including exact reference comparison and checkpoint replay
Compiled NS Same-input, per-step FP32-master comparison with upstream compilation enabled Passed at atol=1e-4, rtol=1e-3; checkpoint replay remains exact
Repository checks All configured pre-commit hooks on the 4 changed files Passed

The compiled comparison is a same-input per-step check. Independently evolving BF16 trajectories can diverge after a rounding difference; these results do not establish bitwise compiled equivalence.

Repository regression command:

TORCHDYNAMO_DISABLE=1 PYTHONPATH=.:tests python -m pytest -q \
  tests/unit/runtime/zero/test_muon_without_zero_optimizer.py \
  -k TestMuonBF16Optimizer

This addresses the missing orthogonalization in BF16_Optimizer. It is separate from #8483's ZeRO-1/2 gradient/momentum dtype reconciliation and #7748's checkpoint dtype conversion.

Fixes #8461.

Signed-off-by: 0z5a <0z5a@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 319f913637

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

see_memory_usage('begin bf16_optimizer', force=True)
self.timers = timers
self.optimizer = init_optimizer
from deepspeed.runtime.zero.muon.muon_optimizer import MuonWithAuxAdam

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the required sign-off trailer

This non-merge commit has no Signed-off-by: trailer, so it does not satisfy the repository's mandatory commit-signing requirement and may be rejected by DCO enforcement. Recreate the commit with --signoff using the configured author identity.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

Comment thread docs/_tutorials/muon-bf16.md Outdated
@@ -0,0 +1,25 @@
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the mandatory license header to the new tutorial

This newly added file begins directly with front matter and omits the license header required for all new files in this repository. Add the prescribed SPDX and DeepSpeed Team header without disrupting the documentation front matter.

AGENTS.md reference: AGENTS.md:L12-L16

Useful? React with 👍 / 👎.

Comment thread deepspeed/runtime/bf16_optimizer.py Outdated
padding_start = max(0, min(partition_size, offset - start))
partition.grad[padding_start:].zero_()
staged[padding_start:].zero_()
staged_momenta.append((committed, staged))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid retaining every staged momentum partition

When an optimizer has multiple Muon parameter groups, each group's full local FP32 momentum clone is appended here and retained until after optimizer.step(). Consequently, the staging peak is proportional to the sum of all local Muon partitions—not the largest group as documented—and can add an entire FP32 model shard in common per-layer or per-rate groupings, causing otherwise viable large-model runs to OOM.

Useful? React with 👍 / 👎.

Comment on lines +173 to +174
# These equal matrices land on exact DP partition boundaries.
assert len(calls) == 2 // deepspeed.comm.get_world_size()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate updates for matrices split across partitions

At world size two, the two 32×32 matrices deliberately end exactly at partition boundaries, so this test never exercises the newly added left/right slicing for a matrix owned by multiple ranks. Counting private kernel calls and checking that some weight changed would still pass if the wrong cross-rank slice were copied; use irregular matrix sizes and compare complete weights and momentum against an independently derived Muon step.

AGENTS.md reference: AGENTS.md:L30-L33

Useful? React with 👍 / 👎.

@delock
delock self-requested a review September 15, 2026 14:06
@delock

delock commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Hi @0z5a , all_gather_into_tensor fetches the full group momentum, but each rank only needs the slices of matrices it owns part of.

In _prepare_muon_updates(), every rank all-gathers the entire group's momentum, including matrices it owns none of. What each rank actually needs is only the cross-partition slices of matrices its partition intersects. A single dist.all_to_all_single with precomputed send/recv split tables (layout is static, computed once in init) delivers exactly that, still as one fused collective:

comm volume drops from ~full group momentum per rank to just the cross-boundary matrix slices;
the gathered workspace shrinks from group-sized to a few boundary matrices.
This also aligns with the granularity of the ZeRO-1/2 fix in #8141, which only pays extra communication for split matrices. If the all-gather is a deliberate trade (few boundary matrices, simpler code), could you note that in the docstring? As written, "one group's gathered momentum is temporary workspace" undersells the staging cost.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
@0z5a
0z5a force-pushed the fix/muon-bf16-optimizer branch from 53911b8 to 6006dc2 Compare September 16, 2026 04:19
@0z5a

0z5a commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the suggestion. I updated the implementation in 6006dc2 to avoid gathering the full momentum buffer.

The BF16 Muon path now precomputes static send/receive split tables and uses a single dist.all_to_all_single per Muon group, exchanging only the matrix slices that cross DP partition boundaries. Each rank reconstructs only the split matrices it owns before applying the full-matrix Muon update.

I also extended the distributed test with irregular matrix shapes that cross the partition boundary and compare the complete weights and gathered momentum against an independent full-matrix reference. In addition, I ran a real 2-GPU BF16 ZeRO-1 training job for 5 optimizer steps with mixed Muon/Adam parameter groups and three microbatches per step. The weights matched the reference exactly at every step (maximum error 0.0), and the gathered momentum matched exactly as well. The full test file passes with 10 tests.

Comment thread docs/_tutorials/muon-bf16.md Outdated
@@ -0,0 +1,25 @@
---
title: "Muon with BF16 weights and FP32 gradient accumulation"

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.

We don't need a seperate tutorial for muon+bf16. Also should not explain internals in tutorial. Can we remove this file?

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.

removed.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
@0z5a
0z5a requested a review from delock September 16, 2026 08:36
@delock
delock enabled auto-merge September 16, 2026 09:25
@delock
delock disabled auto-merge September 16, 2026 14:42
@delock

delock commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

@0z5a can you take a look at test failure? Thanks!

@0z5a

0z5a commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

@0z5a can you take a look at test failure? Thanks!

ok, i will see tomorrow.

Compiler fusion can round BF16 intermediates differently for standalone matrices and partition views. Keep the full-matrix oracle and optimizer step eager for the exact weight and momentum comparisons.

Signed-off-by: 0z5a <0z5a@users.noreply.github.com>
@0z5a

0z5a commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

@0z5a can you take a look at test failure? Thanks!

Fixed CI error, PTAL @delock .

@delock
delock enabled auto-merge September 17, 2026 06:17
@delock
delock added this pull request to the merge queue Sep 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 17, 2026
@0z5a

0z5a commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

@delock , tried to fix again, PTAL.

@delock

delock commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

@0z5a let me investigate sandbox in another context, can you revert sandbox timeout change? Thanks!

@0z5a

0z5a commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Reverted — thanks @delock.

@delock
delock enabled auto-merge September 18, 2026 01:45
@delock
delock added this pull request to the merge queue Sep 18, 2026
Merged via the queue into deepspeedai:master with commit 5880d38 Sep 18, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Muon cannot run under BF16_Optimizer: flat partitions with no Newton-Schulz applied anywhere

2 participants