-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Support configurable parameter, gradient, and optimizer sharding in experimental FSDP v2 #6137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
974fc0e
38e25d3
25b2df8
d19fe48
f22758e
2d523ef
bb9fa5d
93fcd77
d4a8933
afa3a34
64a447a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -109,13 +109,13 @@ def step_post_hook( | |||||||||||
| set_grad(parameter, original_grad) | ||||||||||||
| casted_grads.clear() | ||||||||||||
|
|
||||||||||||
| fsdp_parameter_groups: set[FsdpParameterGroup] = set() | ||||||||||||
| fsdp_parameter_groups: dict[FsdpParameterGroup, None] = {} | ||||||||||||
| for optimizer_group in hooked_optimizer.param_groups: | ||||||||||||
| for parameter in optimizer_group["params"]: | ||||||||||||
| parameter_group = get_containing_parameter_group(parameter) | ||||||||||||
| if parameter_group is None: | ||||||||||||
| continue | ||||||||||||
| fsdp_parameter_groups.add(parameter_group) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd do this instead:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still little confusing: how does this ensure that the order in which communication is issued on each rank is the same?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. synced_parameter_groups is only used for deduplication and is never traversed. As long as the parameter order in |
||||||||||||
| fsdp_parameter_groups[parameter_group] = None | ||||||||||||
|
|
||||||||||||
| for parameter_group in fsdp_parameter_groups: | ||||||||||||
| parameter_group.sync_model_weight_from_main_weight() | ||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the optimizer post hook, we convert the main weights into model weights. In ZeRO-1, the optimizer-facing weights are stored as
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. sync_main_weight_to_model_weight, invoked by the post-optimizer-step hook, does a cast followed by a redistribute today: Megatron-LM/megatron/core/distributed/fsdp/src/megatron_fsdp/experimental/parameter_group.py Lines 209 to 211 in 77c8772
Do we prefer to
My gut feeling is (1), because:
As the last resort, we could also introduce a user knob.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also think option (1) is the better choice. In addition, we should find a clean way to determine whether it is the first microbatch. Perhaps it would make sense to first open a PR specifically to identify the “first microbatch.” 🤔
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Remember #5652 tried to add However, given it's only about redistribution, maybe something like Megatron-LM/megatron/core/distributed/fsdp/src/megatron_fsdp/experimental/parameter_group.py Lines 322 to 323 in ff6b92a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually don’t need the user to specify the first microbatch; we can just determine it ourselves within the param group. |
||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,7 @@ | |
|
|
||
| from ..mixed_precision import MixedPrecisionPolicy | ||
| from .dbuffer import DBuffer | ||
| from .placement import Partial, Placements, Replicate, changed_mesh_axis | ||
| from .placement import Flat, Partial, Placements, Replicate, changed_mesh_axis | ||
|
|
||
| _CONTAINING_PARAMETER_GROUP_ATTR = "_mfsdp_parameter_group" | ||
|
|
||
|
|
@@ -44,6 +44,7 @@ class FsdpParameterGroup: | |
| mesh: DeviceMesh | ||
| dtype: torch.dtype | ||
| requires_grad: bool | ||
| is_first_microbatch: bool | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a big fan of states. Can we infer it from other fields, e.g., placements? |
||
| main_weight: DBuffer | ||
| model_weight: DBuffer | ||
| main_grad: DBuffer | None | ||
|
|
@@ -85,6 +86,7 @@ def __init__( | |
| first_parameter = next(iter(parameters.values())) | ||
| self.dtype = first_parameter.dtype | ||
| self.requires_grad = first_parameter.requires_grad | ||
| self.is_first_microbatch = True | ||
| for name, parameter in parameters.items(): | ||
| if parameter.dtype != self.dtype: | ||
| raise ValueError( | ||
|
|
@@ -146,13 +148,11 @@ def __init__( | |
| dtype=grad_dtype, | ||
| device=self.main_weight.device, | ||
| ) | ||
| self.main_grad.local_buffer.zero_() | ||
| assert self.main_grad.layout == self.main_weight.layout, ( | ||
| "main_grad is built from main_weight tensor shapes on the same mesh, " | ||
| "and DBuffer layouts are deterministic from those shapes and mesh size." | ||
| ) | ||
| # main_grad rests here (DP-outer-Partial for HSDP) between microbatches and | ||
| # is finalized to main_weight's placements after the last microbatch. | ||
| self._accumulation_placements = main_grad_placements | ||
| sharded_parameters: list[nn.Parameter] = [] | ||
| unsharded_parameters: list[nn.Parameter] = [] | ||
| main_grad_dtype = self.main_grad.dtype if self.main_grad is not None else None | ||
|
|
@@ -222,12 +222,10 @@ def unshard_parameters(self) -> None: | |
| gather_axis = changed_mesh_axis( | ||
| self.model_weight.placements, self._unsharded_model_weight.placements | ||
| ) | ||
| if gather_axis is None: | ||
| raise RuntimeError("FSDP parameter unshard requires a changed placement axis.") | ||
| with torch.autograd._unsafe_preserve_version_counter( | ||
| self._unsharded_model_weight.local_buffer | ||
| ): | ||
| if self._symm_mem_pool is not None: | ||
| if self._symm_mem_pool is not None and gather_axis is not None: | ||
| self._unsharded_model_weight.rendezvous(gather_axis) | ||
| self.model_weight.redistribute( | ||
| self._unsharded_model_weight.placements, out=self._unsharded_model_weight | ||
|
|
@@ -283,71 +281,58 @@ def reduce_partial_gradients( | |
| ) -> None: | ||
| """Reduce a packed partial gradient buffer into sharded parameter gradients. | ||
|
|
||
| For HSDP main_grad rests DP-outer-Partial (Partial where main_weight is | ||
| Replicate) between microbatches, accumulating each backward through the | ||
| standard zero_grad contract; the last microbatch reduces the DP-outer axes, | ||
| finalizing main_grad to main_weight's placements so ``.grad`` is the fully | ||
| reduced gradient before ``optimizer.step()``. With every axis Flat (plain | ||
| DP) main_grad already rests finalized. | ||
| main_grad remains in the gradient placements across optimizer steps. The | ||
| last microbatch produces a separate optimizer gradient when its placements | ||
| differ from main_grad. | ||
| """ | ||
| assert self.main_grad is not None | ||
|
|
||
| def has_grad(parameters: tuple[nn.Parameter, ...]) -> bool: | ||
| has_any_grad = False | ||
| has_any_missing_grad = False | ||
| for parameter in parameters: | ||
| if parameter.grad is None: | ||
| has_any_missing_grad = True | ||
| else: | ||
| has_any_grad = True | ||
| if has_any_grad and has_any_missing_grad: | ||
| raise RuntimeError("FSDP sharded gradients must be either all set or all None.") | ||
| return has_any_grad | ||
|
|
||
| # zero_grad(set_to_none=True) clears sharded parameter grads, so this | ||
| # backward can reduce directly into main_grad. zero_grad(set_to_none=False) | ||
| # leaves sharded grads installed, so this backward accumulates into main_grad. | ||
| has_sharded_grads = has_grad(self.sharded_parameters) | ||
|
|
||
| # A non-accumulation main_grad means the previous step finalized it; this | ||
| # only happens on the first microbatch. Redistribute it back to the | ||
| # DP-outer-Partial accumulation placement -- a metadata relabel for HSDP, | ||
| # and a fresh reduce-scattered buffer for HFSDP in the future. | ||
| if self.main_grad.placements != self._accumulation_placements: | ||
| self.main_grad = self.main_grad.redistribute(self._accumulation_placements) | ||
|
|
||
| can_reduce_into_main_grad = ( | ||
| not has_sharded_grads and partial_grad.dtype == self.main_grad.dtype | ||
| ) | ||
| reduce_axis = changed_mesh_axis(partial_grad.placements, self.main_grad.placements) | ||
| if reduce_axis is None: | ||
| raise RuntimeError("FSDP gradient reduction requires a changed placement axis.") | ||
| partial_reduce_op = partial_grad.placements[reduce_axis].reduce_op | ||
| grad_divisor = self.mesh.size(reduce_axis) if partial_reduce_op == dist.ReduceOp.SUM else 1 | ||
| if self._symm_mem_pool is not None: | ||
| partial_grad.rendezvous(reduce_axis) | ||
| if can_reduce_into_main_grad: | ||
| partial_grad.redistribute(self.main_grad.placements, out=self.main_grad) | ||
| if grad_divisor != 1: | ||
| self.main_grad.local_buffer.div_(grad_divisor) | ||
| assert isinstance(self.main_grad.placements[-1], Partial), ( | ||
| "The last placement must be Partial" | ||
| ) | ||
| if self.is_first_microbatch: | ||
| self.main_grad.local_buffer.copy_(partial_grad.local_buffer) | ||
| else: | ||
| self.main_grad.local_buffer.add_(partial_grad.local_buffer) | ||
| else: | ||
| reduced_grad = partial_grad.redistribute(self.main_grad.placements) | ||
| if grad_divisor != 1: | ||
| reduced_grad.local_buffer.div_(grad_divisor) | ||
| if has_sharded_grads: | ||
| self.main_grad.local_buffer.add_(reduced_grad.local_buffer) | ||
| can_reduce_into_main_grad = ( | ||
| self.is_first_microbatch and partial_grad.dtype == self.main_grad.dtype | ||
| ) | ||
| partial_reduce_op = partial_grad.placements[reduce_axis].reduce_op | ||
| grad_divisor = self.mesh.size(reduce_axis) if partial_reduce_op == dist.ReduceOp.SUM else 1 | ||
| if self._symm_mem_pool is not None: | ||
| partial_grad.rendezvous(reduce_axis) | ||
| if can_reduce_into_main_grad: | ||
| partial_grad.redistribute(self.main_grad.placements, out=self.main_grad) | ||
| if grad_divisor != 1: | ||
| self.main_grad.local_buffer.div_(grad_divisor) | ||
| else: | ||
| self.main_grad.local_buffer.copy_(reduced_grad.local_buffer) | ||
| reduced_grad = partial_grad.redistribute(self.main_grad.placements) | ||
| if grad_divisor != 1: | ||
| reduced_grad.local_buffer.div_(grad_divisor) | ||
| if self.is_first_microbatch: | ||
| self.main_grad.local_buffer.copy_(reduced_grad.local_buffer) | ||
| else: | ||
| self.main_grad.local_buffer.add_(reduced_grad.local_buffer) | ||
|
|
||
| optimizer_grad = self.main_grad | ||
| if is_last_microbatch: | ||
| # Finalize the deferred DP-outer reduction (all-reduce for HSDP, | ||
| # reduce-scatter for HFSDP) before binding the sharded parameter grads. | ||
| self.main_grad = self.main_grad.redistribute(self.main_weight.placements) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand why you don't like this because of #6137 (comment). Does #6187 (comment) change your mind? |
||
| optimizer_grad = self.main_grad.redistribute(self.main_weight.placements) | ||
| if optimizer_grad is not self.main_grad: | ||
| self.main_grad.local_buffer.zero_() | ||
|
|
||
| # Make each sharded parameter's .grad consistent with the final main_grad. | ||
| # Install the accumulation grad between microbatches and the finalized grad | ||
| # for optimizer.step() after the last microbatch. | ||
| for index, sharded_parameter in enumerate(self.sharded_parameters): | ||
| sharded_parameter.grad = self.main_grad.get_dtensor(index) | ||
| sharded_parameter.grad = optimizer_grad.get_dtensor(index) | ||
|
|
||
| if is_last_microbatch: | ||
| # Reset for the first microbatch of the next accumulation cycle. | ||
| self.is_first_microbatch = True | ||
| else: | ||
| self.is_first_microbatch = False | ||
|
|
||
| def _get_parameter_owner(module: nn.Module, name: str) -> tuple[nn.Module, str]: | ||
| """Resolve a root-module-relative parameter FQN to its direct owner.""" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.