Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / optimizer / muon.py: 0%
460 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-04 05:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-04 05:18 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
16"""Muon optimizer with HSDP shard-group-aware communication."""
18import math
19from collections.abc import Callable
20from collections import defaultdict
21import logging
22from typing import Any, Dict, List, Optional, Tuple, Union
24import torch
25import torch.distributed as dist
27from hyper_parallel.core.optimizer.optimizer import AsyncReplicateBroadcaster, BaseDistributedOptimizer
28from hyper_parallel.core.optimizer.dtensor_compat import to_local_if_dtensor
29from hyper_parallel.core.optimizer.sharding_category import (
30 HSDPGroupAssignment,
31 ParamShardMeta,
32 build_owner_by_size,
33)
34from hyper_parallel.core.optimizer.muon_shard import (
35 _debug_param_shard_metadata,
36 build_pad_ns_inputs,
37 build_param_shard_metadata_for_group,
38 chunk_update_by_layout,
39 fused_allgather_dtensor_params,
40)
42logger = logging.getLogger(__name__)
44# Legacy: single-coefficient quintic NS (Keller Jordan / Moonlight).
45# Same (a, b, c) applied every step; typically 5 steps.
46_NS_LEGACY_COEFFS: Tuple[float, float, float] = (3.4445, -4.7750, 2.0315)
48# Asym5: 5-step asymmetric NS (dion). Each step uses different coefficients.
49_NS_ASYM5_COEFFS: Tuple[Tuple[float, float, float], ...] = (
50 (4.0848, -6.8946, 2.9270),
51 (3.9505, -6.3029, 2.6377),
52 (3.7418, -5.5913, 2.3037),
53 (2.8769, -3.1427, 1.2046),
54 (2.8366, -3.0525, 1.2012),
55)
58def zeropower_via_newtonschulz5(
59 ns_inputs: torch.Tensor,
60 steps: int,
61 ns_variant: str = "asym5",
62) -> torch.Tensor:
63 """Newton-Schulz orthogonalization with preallocated matmul buffers."""
64 mat_x = ns_inputs
65 transposed = ns_inputs.size(-2) > ns_inputs.size(-1)
66 if transposed:
67 mat_x = mat_x.mT
69 # Normalize input before Newton-Schulz iteration.
70 mat_x = mat_x / (mat_x.norm(dim=(-2, -1), keepdim=True) + 1e-10)
72 n_size = mat_x.size(-2)
73 buf_a = torch.empty(mat_x.shape[:-2] + (n_size, n_size), dtype=mat_x.dtype, device=mat_x.device)
74 buf_b = torch.empty_like(buf_a)
75 buf_c = torch.empty(mat_x.shape, dtype=mat_x.dtype, device=mat_x.device)
77 if ns_variant == "legacy":
78 step_coeffs = [_NS_LEGACY_COEFFS] * steps
79 elif ns_variant == "asym5":
80 step_coeffs = _NS_ASYM5_COEFFS[:steps]
81 else:
82 raise ValueError(
83 f"ns_variant must be 'legacy' or 'asym5', got {ns_variant!r}"
84 )
86 for coeff_a, coeff_b, coeff_c in step_coeffs:
87 torch.matmul(mat_x, mat_x.mT, out=buf_a)
88 torch.matmul(buf_a, buf_a, out=buf_b)
89 buf_a.mul_(coeff_b)
90 buf_b.mul_(coeff_c)
91 buf_a.add_(buf_b)
92 torch.matmul(buf_a, mat_x, out=buf_c)
93 mat_x.mul_(coeff_a).add_(buf_c)
95 del buf_a, buf_b, buf_c
97 if transposed:
98 mat_x = mat_x.mT
100 return mat_x
103def compute_muon_slice_scale(slice_tensor: torch.Tensor, matched_adamw_rms: float) -> float:
104 """Compute Muon scale from the logical matrix dims of a reshaped slice."""
105 shape = tuple(slice_tensor.shape)
106 if len(shape) == 3 and shape[1] == 1:
107 logical_dims = (shape[0], shape[2])
108 else:
109 logical_dims = shape[-2:]
110 return math.sqrt(max(logical_dims)) * matched_adamw_rms
113class Muon(BaseDistributedOptimizer):
114 """Muon optimizer with HSDP shard-group-aware Newton-Schulz orthogonalization.
116 Implements the Muon optimizer which uses Newton-Schulz iteration for matrix
117 orthogonalization of gradient updates, with HSDP-aware communication for
118 sharded parameters.
119 """
121 def __init__(
122 self,
123 params,
124 lr: float = 2e-2,
125 weight_decay: float = 0.1,
126 matched_adamw_rms: float = 0.2,
127 momentum: float = 0.95,
128 nesterov: bool = True,
129 ns_steps: int = 5,
130 ns_variant: str = "asym5",
131 reshape_fn: Optional[Callable[..., Any]] = None,
132 hsdp_replica_count: Optional[Union[int, Tuple[int, ...]]] = None,
133 ):
134 if ns_variant not in ("legacy", "asym5"):
135 raise ValueError(
136 f"ns_variant must be 'legacy' or 'asym5', got {ns_variant!r}"
137 )
138 if not isinstance(momentum, (list, tuple)):
139 momentum = [momentum]
140 if len(momentum) == 1:
141 momentum = (momentum[0], momentum[0])
142 defaults = {
143 "lr": lr,
144 "weight_decay": weight_decay,
145 "matched_adamw_rms": matched_adamw_rms,
146 "momentum": momentum,
147 "nesterov": nesterov,
148 "ns_steps": ns_steps,
149 "ns_variant": ns_variant,
150 }
151 super().__init__(params, defaults, is_muon=True, hsdp_replica_count=hsdp_replica_count)
152 self.reshape_fn = reshape_fn
154 self._group_dtensor_by_mesh()
155 self._build_param_shard_metadata()
156 deduced_count = self._auto_deduce_replica_count()
157 if deduced_count is None:
158 self.hsdp_replica_count = None
159 elif self.hsdp_replica_count is None:
160 self.hsdp_replica_count = deduced_count
161 self._split_replicate_groups()
162 self._build_hsdp_batch()
163 self._build_param_broadcast_info()
164 self._classify_parameters_for_step()
166 def __str__(self):
167 return super().__repr__()
169 __repr__ = __str__
171 @torch.no_grad()
172 def step(self, closure=None) -> Optional[float]:
173 """
174 Perform a single optimization step.
175 De-duplication is controlled by the caller: ``param_to_ns_input`` should already contain only the owned
176 params (via ``hsdp_assign.owned_params``). The caller is responsible for broadcasting the updated params to
177 replica peers via ``AsyncReplicateBroadcaster.flush_group``.
178 """
179 loss = None
180 if closure is not None:
181 with torch.enable_grad():
182 loss = closure()
183 broadcaster = AsyncReplicateBroadcaster(self)
185 num_groups = len(self.param_groups)
187 for group in self.param_groups:
188 group['step'] = (group.get('step') or 0) + 1
190 # Compute momentum only for no-comm params upfront.
191 no_comm_ns: Dict[int, Dict] = {}
192 for group_idx in range(num_groups):
193 info = self._hsdp_assignment_batches.get(group_idx)
194 if not info:
195 # No HSDP info — all params are no_comm.
196 unshard_params = self.unshard_params_by_group.get(group_idx, [])
197 no_comm_ns[group_idx] = self._update_muon_momentum(
198 self.param_groups[group_idx], unshard_params
199 )
200 else:
201 no_comm_params = info.get("no_comm", [])
202 if no_comm_params:
203 no_comm_ns[group_idx] = self._update_muon_momentum(
204 self.param_groups[group_idx], no_comm_params
205 )
206 else:
207 no_comm_ns[group_idx] = {}
209 # Process no-comm params.
210 for group_idx in range(num_groups):
211 if no_comm_ns[group_idx]:
212 self._process_unshard_params(self.param_groups[group_idx], no_comm_ns[group_idx])
214 # Flatten nested batch into a linear schedule.
215 group_linear_batches: Dict[int, List[HSDPGroupAssignment]] = {}
216 max_num_batches = 0
217 for group_idx in range(num_groups):
218 info = self._hsdp_assignment_batches.get(group_idx)
219 linear_batches = []
220 if info:
221 for bg in info.get("batch_groups", []):
222 linear_batches.extend(bg.get("sub_batches", []))
223 group_linear_batches[group_idx] = linear_batches
224 max_num_batches = max(max_num_batches, len(linear_batches))
226 # Process batches: compute momentum per-batch for owned params only (HSDP de-duplication).
227 for batch_idx in range(max_num_batches):
228 for group_idx in range(num_groups):
229 linear_batches = group_linear_batches[group_idx]
230 if batch_idx >= len(linear_batches):
231 continue
233 hsdp_assign = linear_batches[batch_idx]
234 group = self.param_groups[group_idx]
236 # Compute momentum for this assignment's owned params only.
237 ns_inputs = self._update_muon_momentum(group, hsdp_assign.owned_params)
238 if not hsdp_assign.is_shard:
239 if ns_inputs:
240 self._process_unshard_params(group, ns_inputs)
241 else:
242 self._process_shard_params(
243 group, ns_inputs, [hsdp_assign], group_idx,
244 buffer_cache={},
245 )
247 # Flush broadcasts after each assignment.
248 broadcaster.flush_group(hsdp_assign)
250 broadcaster.wait_all()
251 return loss
253 def _classify_parameters_for_step(self) -> None:
254 """Classify params by whether the last two dims are sharded.
256 unshard: run Newton-Schulz locally.
257 shard: all-gather before Newton-Schulz.
259 Reads from self._hsdp_assignment_batches which is organized by batch.
260 """
261 self.unshard_params_by_group: Dict[int, List] = {}
262 self.shard_params_by_group: Dict[int, List] = {}
263 self.shard_assignments_by_group: Dict[int, List[HSDPGroupAssignment]] = {}
264 # Per group: record.index -> shard coord that computes NS.
265 self._shard_compute_coord: Dict[int, Dict[int, Tuple[int, ...]]] = {}
267 for group_idx, group in enumerate(self.param_groups):
268 assignment_info = self._hsdp_assignment_batches.get(group_idx)
270 unshard_params = []
271 shard_hsdp_assignments: List[HSDPGroupAssignment] = []
273 if assignment_info:
274 unshard_params.extend(assignment_info["no_comm"])
275 for bg in assignment_info["batch_groups"]:
276 for hsdp_assign in bg["sub_batches"]:
277 if hsdp_assign.is_shard:
278 shard_hsdp_assignments.append(hsdp_assign)
279 else:
280 unshard_params.extend(hsdp_assign.owned_params)
281 else:
282 unshard_params.extend(group["params"])
284 # Shard params only include locally owned params; replica ownership is enforced at apply time.
285 shard_params = [p for a in shard_hsdp_assignments for p in a.owned_params]
287 # Greedily assign NS compute across shard ranks.
288 self._shard_compute_coord[group_idx] = {}
289 for hsdp_assign in shard_hsdp_assignments:
290 shard_sizes, _, _, _ = self._get_shard_info(hsdp_assign)
291 compute_by_index = build_owner_by_size(
292 records=hsdp_assign.owned_records,
293 replicate_sizes=shard_sizes,
294 )
295 self._shard_compute_coord[group_idx].update(compute_by_index)
297 self.unshard_params_by_group[group_idx] = unshard_params
298 self.shard_assignments_by_group[group_idx] = shard_hsdp_assignments
299 self.shard_params_by_group[group_idx] = shard_params
301 def _update_muon_momentum(
302 self,
303 group: Dict[str, Any],
304 params: List[torch.Tensor],
305 ) -> Dict[torch.nn.Parameter, torch.Tensor]:
306 """Compute first-order momentum and return bfloat16 NS inputs."""
307 momentum1, momentum2 = group["momentum"]
308 nesterov = group['nesterov']
310 # Pre-filter params with valid grads and ensure momentum buffers exist
311 valid_params = []
312 grads = []
313 bufs = []
314 for p in params:
315 g = p.grad
316 if g is None:
317 continue
318 state = self.state[p]
319 if "momentum_buffer" not in state:
320 state["momentum_buffer"] = torch.zeros_like(g)
321 valid_params.append(p)
322 grads.append(g)
323 bufs.append(state["momentum_buffer"])
325 if not valid_params:
326 return {}
328 # Match muon_update_core():
329 # m_for_update = grad + momentum1 * m_old
330 # m_new = grad + momentum2 * m_old
331 local_grads = [to_local_if_dtensor(g) for g in grads]
332 local_bufs = [to_local_if_dtensor(b) for b in bufs]
333 # pylint: disable=protected-access
334 local_us = torch._foreach_add(local_grads, local_bufs, alpha=momentum1)
336 torch._foreach_mul_(local_bufs, momentum2)
337 torch._foreach_add_(local_bufs, local_grads)
339 if nesterov:
340 torch._foreach_mul_(local_us, momentum1)
341 torch._foreach_add_(local_us, local_grads)
343 if local_us[0].dtype == torch.bfloat16:
344 local_us_bf = local_us
345 else:
346 local_us = list(local_us)
347 for i, u in enumerate(local_us):
348 local_us[i] = u.to(torch.bfloat16)
349 local_us_bf = local_us
351 return dict(zip(valid_params, local_us_bf))
353 def _process_unshard_params(
354 self,
355 group: Dict[str, Any],
356 param_to_ns_input: Dict[torch.nn.Parameter, torch.Tensor],
357 ) -> None:
358 """Process un-sharded params: shape-group -> memory-batch -> NS -> local update."""
359 lr = group["lr"]
360 weight_decay = group["weight_decay"]
362 # empty params are not processed in muon, for uneven shard
363 param_to_ns_input = {
364 p: ns_input for p, ns_input in param_to_ns_input.items() if ns_input.numel() > 0
365 }
366 if not param_to_ns_input:
367 return
369 shape_groups = self._group_by_shape(list(param_to_ns_input.keys()))
370 for _, p_list in shape_groups.items():
371 safe_batches = self._split_into_memory_safe_batches(p_list, shard_size=1)
373 for sub_batch in safe_batches:
374 updates_dict = self._compute_batched_ns_updates(
375 sub_batch, param_to_ns_input, group, no_shard=True
376 )
378 local_params = [to_local_if_dtensor(p.data) for p in sub_batch]
379 local_updates = [updates_dict[p].view(lp.shape) for p, lp in zip(sub_batch, local_params)]
381 if weight_decay != 0.0:
382 # pylint: disable=protected-access
383 torch._foreach_mul_(local_params, 1 - lr * weight_decay)
384 # pylint: disable=protected-access
385 torch._foreach_add_(local_params, local_updates, alpha=-lr)
387 def _gather_and_compute_shard_updates(
388 self,
389 valid_params: List[torch.nn.Parameter],
390 param_to_ns_input: Dict[torch.nn.Parameter, torch.Tensor],
391 hsdp_assign: HSDPGroupAssignment,
392 shard_compute_coord: Dict[int, Tuple[int, ...]],
393 group: Dict[str, Any],
394 buffer_cache: Optional[Dict],
395 ) -> Tuple[
396 Dict[torch.nn.Parameter, torch.Tensor],
397 Dict[torch.nn.Parameter, Tuple[int, ...]],
398 ]:
399 """Gather NS inputs and compute updates for locally-assigned shard params.
401 Returns:
402 (my_updates, param_compute_coord) for this HSDP assignment.
403 """
404 shard_sizes, local_coords, shard_pgs, total_shard_size = self._get_shard_info(hsdp_assign)
405 param_to_index = {record.param: record.index for record in hsdp_assign.owned_records}
407 my_params: List[torch.nn.Parameter] = []
408 my_param_ids: set = set()
409 param_compute_coord: Dict[torch.nn.Parameter, Tuple[int, ...]] = {}
410 my_indices: set = set()
412 for idx, p in enumerate(valid_params):
413 p_index = param_to_index[p]
414 compute_coord = shard_compute_coord.get(p_index, (0,) * len(shard_sizes))
415 param_compute_coord[p] = compute_coord
416 if compute_coord == local_coords:
417 my_params.append(p)
418 my_param_ids.add(id(p))
419 my_indices.add(idx)
421 # Fused all-gather full NS inputs; keep only tensors computed locally.
422 gathered_inputs: Dict[torch.nn.Parameter, torch.Tensor] = {}
423 local_inputs = build_pad_ns_inputs(
424 valid_params,
425 param_to_ns_input,
426 self._param_shard_metadata,
427 )
428 param_shard_metadata = [self._param_shard_metadata.get(p) for p in valid_params]
429 gathered_list = fused_allgather_dtensor_params(
430 local_inputs, shard_pgs, hsdp_assign.layout_spec,
431 param_shard_metadata=param_shard_metadata,
432 buffer_cache=buffer_cache,
433 keep_indices=my_indices,
434 )
435 for p, full_inp in zip(valid_params, gathered_list):
436 if id(p) in my_param_ids:
437 gathered_inputs[p] = full_inp
438 local_inputs.clear()
439 gathered_list.clear()
441 # Compute NS updates in shape groups and memory-safe batches.
442 my_updates: Dict[torch.nn.Parameter, torch.Tensor] = {}
443 if my_params:
444 shape_groups = self._group_by_shape(my_params)
445 for _, p_list in shape_groups.items():
446 safe_batches = self._split_into_memory_safe_batches(p_list, shard_size=total_shard_size)
447 for sub_batch in safe_batches:
448 updates_dict = self._compute_batched_ns_updates(
449 sub_batch, gathered_inputs, group
450 )
451 for p in sub_batch:
452 my_updates[p] = updates_dict[p].contiguous()
453 del updates_dict
454 gathered_inputs.clear()
456 return my_updates, param_compute_coord
458 def _process_shard_params(
459 self,
460 group: Dict[str, Any],
461 param_to_ns_input: Dict[torch.nn.Parameter, torch.Tensor],
462 hsdp_assignments: List[HSDPGroupAssignment],
463 group_idx: int,
464 buffer_cache: Optional[Dict] = None,
465 ) -> None:
466 """Process sharded params with greedy shard-group compute assignment."""
467 shard_compute_coord = self._shard_compute_coord.get(group_idx, {})
469 for hsdp_assign in hsdp_assignments:
470 owned_params = hsdp_assign.owned_params
471 # Communication membership is static per assignment. Ranks with empty
472 # local shards still participate via build_pad_ns_inputs().
473 valid_params = owned_params
475 shard_sizes, _, shard_pgs, total_shard_size = self._get_shard_info(hsdp_assign)
477 # uneven shard check
478 shard_pgs_rank = []
479 for pg in shard_pgs:
480 pg_rank = torch.distributed.get_process_group_ranks(pg)
481 shard_pgs_rank.append(pg_rank)
483 for p in valid_params:
484 if tuple(p.shape)[0] % shard_sizes[0] != 0:
485 logger.debug_rank0(
486 "[hyper-optimizer uneven-shard] p %s placement %s device_mesh %s "
487 "local_shape %s global_shape %s pg_rank %s shard_sizes %s",
488 p.model_name,
489 p.placements,
490 p.device_mesh,
491 p.to_local().shape,
492 p.shape,
493 shard_pgs_rank,
494 shard_sizes,
495 )
497 if valid_params:
498 safe_batches = self._split_into_memory_safe_batches(
499 valid_params, shard_size=total_shard_size,
500 )
501 for sub_batch in safe_batches:
502 my_updates, param_compute_coord = self._gather_and_compute_shard_updates(
503 sub_batch, param_to_ns_input, hsdp_assign,
504 shard_compute_coord, group, buffer_cache,
505 )
507 self._fused_broadcast_and_apply(
508 sub_batch, my_updates, param_compute_coord,
509 group, hsdp_assign,
510 )
512 def _group_by_shape(
513 self,
514 params: List[torch.nn.Parameter],
515 ) -> Dict[tuple, List[torch.nn.Parameter]]:
516 """Group parameters by their last-2-dim shape (A, B) for batched NS.
518 [1024, 1024], [1024, 1, 1024], and [3, 1024, 1024] all map to
519 key (1024, 1024) for maximum batch merging.
520 """
521 groups = defaultdict(list)
523 for p in params:
524 core_shape = self._shape_to_core_shape(tuple(p.shape))
525 groups[core_shape].append(p)
527 return groups
529 @staticmethod
530 def _shape_to_core_shape(shape: Tuple[int, ...]) -> Tuple[int, int]:
531 if len(shape) == 2:
532 return (shape[0], shape[1])
533 if len(shape) == 3 and shape[1] == 1:
534 return (shape[0], shape[2])
535 if len(shape) >= 3:
536 return (shape[-2], shape[-1])
537 raise ValueError('1D parameters are not supported in Muon')
539 def _reshape_ns_input(
540 self,
541 param: torch.nn.Parameter,
542 ns_input: torch.Tensor,
543 ) -> Tuple[torch.Tensor, List[torch.Tensor]]:
544 """Return the contiguous NS input and any reshape views used for NS."""
545 working_input = ns_input if ns_input.is_contiguous() else ns_input.contiguous()
546 if self.reshape_fn is None:
547 return working_input, [working_input]
549 param_fqn = param.model_name
550 if param_fqn is None:
551 return working_input, [working_input]
553 reshaped_inputs = list(self.reshape_fn(param_fqn, working_input))
554 if not reshaped_inputs:
555 return working_input, [working_input]
557 if reshaped_inputs[0].shape != working_input.shape:
558 logger.info_rank0(
559 "Reshape %s from %s to %s",
560 param_fqn,
561 working_input.shape,
562 reshaped_inputs[0].shape,
563 )
565 for reshaped_input in reshaped_inputs:
566 assert (
567 reshaped_input.untyped_storage().data_ptr() == working_input.untyped_storage().data_ptr()
568 ), "reshape_fn must return views that share storage with the working NS input tensor."
570 return working_input, reshaped_inputs
572 def _compute_batched_ns_outputs_for_tensors(
573 self,
574 tensor_list: List[torch.Tensor],
575 ns_steps: int,
576 ns_variant: str = "asym5",
577 ) -> List[torch.Tensor]:
578 """Run batched NS on mixed-shape tensors and restore their original shapes."""
579 if not tensor_list:
580 return []
582 inputs_3d = []
583 slice_sizes = []
584 shapes_info = []
586 for tensor in tensor_list:
587 origin_shape = tuple(tensor.shape)
588 is_conv = False
589 if len(origin_shape) == 2:
590 inp_3d = tensor.unsqueeze(0)
591 n_dim = 1
592 elif len(origin_shape) == 3 and origin_shape[1] == 1:
593 inp_3d = tensor.squeeze(1).unsqueeze(0)
594 is_conv = True
595 n_dim = 1
596 elif len(origin_shape) == 3:
597 inp_3d = tensor
598 n_dim = origin_shape[0]
599 else:
600 inp_3d = tensor.reshape(-1, origin_shape[-2], origin_shape[-1])
601 n_dim = inp_3d.shape[0]
603 inputs_3d.append(inp_3d)
604 slice_sizes.append(n_dim)
605 shapes_info.append((origin_shape, is_conv))
607 merged_input = torch.cat(inputs_3d, dim=0)
608 squeeze_batch = merged_input.shape[0] == 1
609 if squeeze_batch:
610 merged_input = merged_input.squeeze(0)
612 merged_update = zeropower_via_newtonschulz5(merged_input, steps=ns_steps, ns_variant=ns_variant)
613 del merged_input
615 if squeeze_batch:
616 merged_update = merged_update.unsqueeze(0)
618 outputs = []
619 current_idx = 0
620 for n_dim, (origin_shape, is_conv) in zip(slice_sizes, shapes_info):
621 update = merged_update[current_idx: current_idx + n_dim]
622 current_idx += n_dim
624 if is_conv:
625 update = update.squeeze(0).unsqueeze(1)
626 elif len(origin_shape) == 2:
627 update = update.squeeze(0)
628 elif len(origin_shape) >= 4:
629 update = update.reshape(origin_shape)
631 outputs.append(update)
633 del merged_update
634 return outputs
636 def _split_into_memory_safe_batches(
637 self,
638 p_list: List[torch.nn.Parameter],
639 shard_size: int = 1,
640 ) -> List[List[torch.nn.Parameter]]:
641 """Split parameters into memory-safe batches to prevent OOM during NS.
643 The per-batch element limit is scaled down by shard_size to account
644 for the memory amplification from allgather.
645 """
646 max_numel_per_batch = 512 * 1024 * 1024 // shard_size
648 batches = []
649 current_batch = []
650 current_count = 0
652 for p in p_list:
653 p_count = p.numel()
654 if current_batch and current_count + p_count > max_numel_per_batch:
655 batches.append(current_batch)
656 current_batch = [p]
657 current_count = p_count
658 else:
659 current_batch.append(p)
660 current_count += p_count
662 if current_batch:
663 batches.append(current_batch)
665 return batches
667 def _compute_batched_ns_updates(
668 self,
669 p_list: List[torch.nn.Parameter],
670 ns_inputs: Dict[torch.nn.Parameter, torch.Tensor],
671 group: Dict[str, Any],
672 no_shard: bool = False
673 ) -> Dict[torch.nn.Parameter, torch.Tensor]:
674 """Batched Newton-Schulz update for mixed 2D / Conv3D / 3D parameters.
676 Normalizes all inputs to 3D, concatenates along dim 0, runs a single
677 NS iteration, then slices results back to original shapes.
679 """
680 updates_dict = {}
682 if not p_list:
683 return updates_dict
685 rms = group["matched_adamw_rms"]
686 ns_steps = group["ns_steps"]
687 ns_variant = group["ns_variant"]
689 reshape_groups: Dict[Tuple[int, int], List[torch.Tensor]] = defaultdict(list)
690 origin_shapes: Dict[torch.nn.Parameter, Tuple[int, ...]] = {}
691 working_inputs: Dict[torch.nn.Parameter, torch.Tensor] = {}
693 for p in p_list:
694 origin_shape = tuple(getattr(p, 'local_shape', None) or p.to_local().shape) if no_shard else tuple(p.shape)
695 ns_input = ns_inputs[p].view(origin_shape)
696 origin_shapes[p] = origin_shape
698 working_input, reshaped_inputs = self._reshape_ns_input(p, ns_input)
699 working_inputs[p] = working_input
700 for reshaped_input in reshaped_inputs:
701 core_shape = self._shape_to_core_shape(tuple(reshaped_input.shape))
702 reshape_groups[core_shape].append(reshaped_input)
704 for _, tensor_list in reshape_groups.items():
705 reshaped_updates = self._compute_batched_ns_outputs_for_tensors(
706 tensor_list,
707 ns_steps,
708 ns_variant=ns_variant,
709 )
711 # scale updates
712 for reshaped_input, reshaped_update in zip(tensor_list, reshaped_updates):
713 slice_scale = compute_muon_slice_scale(reshaped_update, rms)
714 reshaped_update.mul_(slice_scale)
715 reshaped_input.copy_(reshaped_update.contiguous().view_as(reshaped_input))
717 for p in p_list:
718 ns_input = ns_inputs[p].view(origin_shapes[p])
719 working_input = working_inputs[p]
720 if working_input.untyped_storage().data_ptr() != ns_input.untyped_storage().data_ptr():
721 ns_input.copy_(working_input)
722 updates_dict[p] = ns_input
724 return updates_dict
726 def _fused_broadcast_and_apply(
727 self,
728 valid_params: List[torch.nn.Parameter],
729 my_updates: Dict[torch.nn.Parameter, torch.Tensor],
730 param_compute_coord: Dict[torch.nn.Parameter, Tuple[int, ...]],
731 group: Dict[str, Any],
732 hsdp_assign: HSDPGroupAssignment,
733 ) -> None:
734 """Fused broadcast and apply for shard-group updates."""
735 lr = group["lr"]
736 weight_decay = group["weight_decay"]
737 shard_sizes, local_coords, shard_pgs, total_shard_size = self._get_shard_info(hsdp_assign)
738 device = to_local_if_dtensor(valid_params[0].data).device
740 coord_groups: Dict[Tuple[int, ...], List[torch.nn.Parameter]] = defaultdict(list)
741 for p in valid_params:
742 coord_groups[param_compute_coord[p]].append(p)
744 all_local_params: List[torch.Tensor] = []
745 all_update_shards: List[torch.Tensor] = []
747 alignment_bytes = 512
748 element_size = torch.empty(0, dtype=torch.bfloat16, device=device).element_size()
749 alignment_elements = max(1, alignment_bytes // element_size)
751 pack_buffers: Dict[Tuple[int, ...], torch.Tensor] = {}
752 coord_param_offsets: Dict[Tuple[int, ...], List[Tuple[int, int, int]]] = {}
754 for coord, coord_params in coord_groups.items():
755 is_compute_rank = coord == local_coords
756 param_offsets: List[Tuple[int, int, int]] = []
757 total_padded_numel = 0
759 for p in coord_params:
760 actual_numel = p.numel()
761 padded_numel = ((actual_numel + alignment_elements - 1) // alignment_elements) * alignment_elements
762 param_offsets.append((total_padded_numel, actual_numel, padded_numel))
763 total_padded_numel += padded_numel
765 coord_param_offsets[coord] = param_offsets
766 pack_buffer = torch.empty(total_padded_numel, dtype=torch.bfloat16, device=device)
768 if is_compute_rank:
769 for p, (offset, actual_numel, padded_numel) in zip(coord_params, param_offsets):
770 update = my_updates[p]
771 pack_buffer[offset:offset + actual_numel].copy_(update.reshape(-1))
772 if padded_numel > actual_numel:
773 pack_buffer[offset + actual_numel:offset + padded_numel].zero_()
775 pack_buffers[coord] = pack_buffer
777 if total_shard_size > 1:
778 self._batched_relay_broadcast(
779 pack_buffers, shard_pgs, shard_sizes, local_coords
780 )
782 layout_spec = hsdp_assign.layout_spec
783 for coord, coord_params in coord_groups.items():
784 pack_buffer = pack_buffers[coord]
785 param_offsets = coord_param_offsets[coord]
787 for p, (offset, actual_numel, _) in zip(coord_params, param_offsets):
788 full_update = pack_buffer[offset:offset + actual_numel].view(tuple(p.shape))
789 update_to_apply = chunk_update_by_layout(
790 full_update,
791 p,
792 layout_spec,
793 self._param_shard_metadata.get(p),
794 )
796 local_param = to_local_if_dtensor(p.data)
797 all_local_params.append(local_param)
798 all_update_shards.append(update_to_apply.view(local_param.shape))
800 if not all_local_params:
801 return
803 if weight_decay != 0.0:
804 coeff = 1.0 - lr * weight_decay
805 # pylint: disable=protected-access
806 torch._foreach_mul_(all_local_params, coeff)
808 # Slice-wise Muon scaling has already been applied during NS postprocess.
809 # pylint: disable=protected-access
810 torch._foreach_add_(all_local_params, all_update_shards, alpha=-lr)
812 def _build_param_shard_metadata(self) -> None:
813 """Build shard metadata once during optimizer init."""
814 self._param_shard_metadata: Dict[torch.nn.Parameter, ParamShardMeta] = {}
816 for _, hsdp_groups in self._hsdp_grouping.values():
817 for hsdp_group in hsdp_groups:
818 if hsdp_group.layout_spec is None or not hsdp_group.layout_spec.shard_axes:
819 continue
820 group_param_to_meta = build_param_shard_metadata_for_group(hsdp_group)
821 for param, shard_meta in group_param_to_meta.items():
822 self._param_shard_metadata[param] = shard_meta
823 _debug_param_shard_metadata(hsdp_group, group_param_to_meta)
825 @staticmethod
826 def _get_shard_info(
827 hsdp_assign: HSDPGroupAssignment,
828 ) -> Tuple[Tuple[int, ...], Tuple[int, ...], Tuple[dist.ProcessGroup, ...], int]:
829 """Extract shard topology from HSDPGroupAssignment.
831 Returns:
832 shard_sizes: Size of each shard mesh dimension.
833 local_coords: Current rank's coordinate in each shard mesh dim.
834 shard_pgs: ProcessGroup for each shard mesh dim.
835 total_shard_size: Product of all shard_sizes.
836 """
837 shard_pgs = hsdp_assign.shard_pgs
838 shard_sizes = tuple(
839 dist.get_world_size(pg) if pg is not None else 1
840 for pg in shard_pgs
841 )
842 local_coords = tuple(
843 dist.get_rank(pg) if pg is not None else 0
844 for pg in shard_pgs
845 )
846 total_shard_size = 1
847 for s in shard_sizes:
848 total_shard_size *= s
850 return shard_sizes, local_coords, shard_pgs, total_shard_size
852 @staticmethod
853 def _batched_relay_broadcast(
854 tensor_dict: Dict[Tuple[int, ...], torch.Tensor],
855 shard_pgs: Tuple[dist.ProcessGroup, ...],
856 shard_sizes: Tuple[int, ...],
857 local_coords: Tuple[int, ...],
858 ) -> None:
859 """
860 Batched asynchronous multi-dimensional relay broadcast.
861 By operating asynchronously within each dimension, we eliminate CPU overhead bubbles
862 while strictly preserving the multidimensional relay dependency.
863 """
864 for dim_idx, pg in enumerate(shard_pgs):
865 if pg is None or shard_sizes[dim_idx] <= 1:
866 continue
868 work_handles = []
870 for coord, tensor in tensor_dict.items():
871 aligned = all(
872 local_coords[sub_dim] == coord[sub_dim]
873 for sub_dim in range(dim_idx + 1, len(shard_pgs))
874 )
875 if not aligned:
876 continue
878 src_rank_in_pg = coord[dim_idx]
879 global_src_rank = dist.get_global_rank(pg, src_rank_in_pg)
881 work = dist.broadcast(tensor, src=global_src_rank, group=pg, async_op=True)
882 if work is not None:
883 work_handles.append(work)
885 for work in work_handles:
886 work.wait()