-
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 3 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" | ||||
|
|
||||
|
|
@@ -146,6 +146,7 @@ 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." | ||||
|
|
@@ -222,12 +223,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 | ||||
|
|
@@ -250,11 +249,13 @@ def release_unsharded_storage(self) -> None: | |||
| # so keep the shared storage-release path. | ||||
| self._unsharded_model_weight.release_storage() | ||||
|
|
||||
| def _install_sharded_grads(self) -> None: | ||||
| def _install_sharded_grads(self, grad_buffer: DBuffer | None = None) -> None: | ||||
| """Point each sharded parameter's grad at main_grad's current DTensor view.""" | ||||
| assert self.main_grad is not None | ||||
| if grad_buffer is None: | ||||
| grad_buffer = self.main_grad | ||||
| assert grad_buffer is not None | ||||
| for index, sharded_parameter in enumerate(self.sharded_parameters): | ||||
| sharded_parameter.grad = self.main_grad.get_dtensor(index) | ||||
| sharded_parameter.grad = grad_buffer.get_dtensor(index) | ||||
|
|
||||
| def allocate_partial_grad_buffer(self) -> DBuffer: | ||||
| """Allocate the unreduced reduce-scatter input buffer.""" | ||||
|
|
@@ -298,6 +299,29 @@ def reduce_partial_gradients( | |||
| """ | ||||
| assert self.main_grad is not None | ||||
|
|
||||
| optimizer_axis = changed_mesh_axis( | ||||
| self.main_grad.placements, self.main_weight.placements | ||||
| ) | ||||
| if ( | ||||
| optimizer_axis is not None | ||||
| and isinstance(self.main_grad.placements[optimizer_axis], Replicate) | ||||
| and isinstance(self.main_weight.placements[optimizer_axis], Flat) | ||||
| ): | ||||
| with self._symmetric_memory_context(): | ||||
| partial_grad = partial_grad.cast(self.main_grad.dtype) | ||||
| if not is_last_microbatch: | ||||
| self.main_grad.local_buffer.add_(partial_grad.local_buffer) | ||||
| return | ||||
| partial_grad.local_buffer.add_(self.main_grad.local_buffer) | ||||
|
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. For no_shard and optim, MFSDP v2 currently accumulates
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. Yes, I'm aware of this unimplemented optimization. I believe it can be added by allocating reduce_scatter input before calling the fused backward+copy_in kernel. In fact, for exactly this extension, I chose to launch backward and copy_in on the same stream: http://nv/mfsdp-schedule-design => alternatives considered => FSDP2. |
||||
| if self._symm_mem_pool is not None: | ||||
| partial_grad.rendezvous(optimizer_axis) | ||||
| optimizer_grad = partial_grad.redistribute(self.main_weight.placements) | ||||
| if partial_grad.placements[optimizer_axis].reduce_op == dist.ReduceOp.SUM: | ||||
| optimizer_grad.local_buffer.div_(self.mesh.size(optimizer_axis)) | ||||
| self._install_sharded_grads(optimizer_grad) | ||||
|
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. Here, we install
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. #6041 should make this unnecessary.
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. let me refine this PR with #6041 🤔
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. #6041 has been merged FYI
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. This is still fairly subtle. Take ZeRO-1 as an example. During gradient accumulation, main_grad uses a Partial placement. On the last microbatch, it is finalized with a reduce-scatter and becomes the optimizer-facing gradient with a Flat placement. Therefore, the lifecycle contains two transitions: Partial -> Flat on the last microbatch, followed by Flat -> Partial on the first microbatch of the next accumulation cycle. These transitions should be handled differently. Partial -> Flat requires a real reduce-scatter and allocates a new output DBuffer. In contrast, Flat -> Partial should not perform communication: the Flat gradient belongs to the previous optimizer step and can be discarded after the optimizer has consumed it, so we can directly allocate a fresh Partial accumulation buffer. The storage dependencies must still be maintained correctly. We need a record stream: Since main_grad is replaced by a new DBuffer every global batch—either returned by redistribute() or allocated directly—could its storage address become unstable? Would CUDA graph capture or fused gradient accumulation require this address to remain stable across iterations?
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 think an alternative approach is to create an accumulate grad (which uses partial/replicate placement) and separate it from the main grad (which is the flat shard), but let them share the same storage — for example, the main grad would be a view of the accumulate grad. @wujingyue What do you think?
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 doubt this. All these operations in the above diagram are enqueued to the same, reduce-scatter stream. So it should be safe to free after last use. https://dev-discuss.pytorch.org/t/fsdp-cudacachingallocator-an-outsider-newb-perspective/1486#p-2441-v1-single-stream-no-nonsense-cudacachingallocator-3
Try our best not to. The whole blogpost is pretty much to teach people not to
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 take this back. I think you might have found the root cause of #5956 (comment). Megatron-LM/megatron/core/distributed/fsdp/src/megatron_fsdp/experimental/parameter_group.py Line 345 in 8339200
main_grad on a different stream than it was allocated, which invites race conditions. I'll figure out a way forward ASAP.
cc @Achyuthan-S as well for the HFSDP CI failure he's investigating |
||||
| self.main_grad.local_buffer.zero_() | ||||
| return | ||||
|
|
||||
| def has_grad(parameters: tuple[nn.Parameter, ...]) -> bool: | ||||
| has_any_grad = False | ||||
| has_any_missing_grad = False | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.