Repository navigation
GroupedTensor Machinery for MXFP8 Dispatch input passed from Megatron #3660
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
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 |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| """Grouped tensor class for handling collections of tensors with different shapes""" | ||
| from __future__ import annotations | ||
|
|
||
| import weakref | ||
| from typing import List, Optional, Tuple | ||
|
|
||
| import torch | ||
|
|
@@ -66,6 +67,48 @@ def backward(ctx, grad_output: torch.Tensor): | |
| } | ||
|
|
||
|
|
||
| class GroupedTensorUntypedStorage(torch.UntypedStorage): | ||
| """Storage facade that resizes grouped data and scale storages.""" | ||
|
|
||
| def __new__(cls, owner: "GroupedTensor"): | ||
| instance = super().__new__(cls, 0, device=owner.device) | ||
| instance._aliases = [] | ||
| instance.register_alias(owner) | ||
| return instance | ||
|
|
||
| def register_alias(self, tensor: "GroupedTensor") -> None: | ||
| """Register a wrapper that shares this storage's payload allocations.""" | ||
| self._aliases.append(weakref.ref(tensor)) | ||
|
Comment on lines
+79
to
+81
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.
|
||
|
|
||
| def resize_(self, size: int): | ||
| """Resize grouped payload storages when pseudo-deallocated by a scheduler.""" | ||
| if size != 0: | ||
| raise RuntimeError( | ||
| f"GroupedTensor untyped storage only supports resize_(0), got resize_({size})" | ||
| ) | ||
| resized_storages = set() | ||
| live_aliases = [] | ||
| for alias_ref in self._aliases: | ||
| alias = alias_ref() | ||
| if alias is not None: | ||
| live_aliases.append(alias_ref) | ||
| for tensor in ( | ||
| alias.rowwise_data, | ||
| alias.columnwise_data, | ||
| alias.scale_inv, | ||
| alias.columnwise_scale_inv, | ||
| ): | ||
| if not isinstance(tensor, torch.Tensor): | ||
| continue | ||
| storage = tensor.untyped_storage() | ||
| storage_id = storage._cdata | ||
| if storage_id not in resized_storages: | ||
| storage.resize_(0) | ||
| resized_storages.add(storage_id) | ||
| self._aliases = live_aliases | ||
| return self | ||
|
|
||
|
|
||
| class GroupedTensor(GroupedTensorStorage, torch.Tensor): | ||
| """Tensor wrapper class for grouped tensor storage.""" | ||
|
|
||
|
|
@@ -174,6 +217,7 @@ def __new__( | |
| nvfp4_e4m3_max=nvfp4_e4m3_max, | ||
| scale_inv_dtype=scale_inv_dtype, | ||
| ) | ||
| instance._untyped_storage = GroupedTensorUntypedStorage(instance) | ||
| return instance | ||
|
|
||
| @classmethod | ||
|
|
@@ -208,8 +252,10 @@ def copy_grouped_storage_metadata(dst: GroupedTensor, src: GroupedTensor) -> Non | |
| dst.nvfp4_use_4over6 = src.nvfp4_use_4over6 | ||
| dst.nvfp4_e4m3_max = src.nvfp4_e4m3_max | ||
| dst.scale_inv_dtype = src._scale_inv_dtype | ||
| dst._untyped_storage = src._untyped_storage | ||
| dst._untyped_storage.register_alias(dst) | ||
|
|
||
| def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor: | ||
| def make_wrapper_like(src: GroupedTensor) -> GroupedTensor: | ||
| """Create a wrapper of the same type and tensor metadata as src.""" | ||
| out = torch.Tensor._make_wrapper_subclass( | ||
| type(src), | ||
|
|
@@ -218,7 +264,7 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor: | |
| storage_offset=src.storage_offset(), | ||
| dtype=src.dtype, | ||
| layout=src.layout, | ||
| requires_grad=requires_grad, | ||
| requires_grad=False, | ||
| device=src.device, | ||
| ) | ||
| copy_grouped_storage_metadata(out, src) | ||
|
|
@@ -229,9 +275,24 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor: | |
| src = args[0] | ||
| if not isinstance(src, GroupedTensor): | ||
| raise TypeError(f"Expected GroupedTensor, got {type(src).__name__}") | ||
| if func == torch.ops.aten.detach.default: | ||
| return make_wrapper_like(src, requires_grad=False) | ||
| return make_wrapper_like(src, requires_grad=src.requires_grad) | ||
| return make_wrapper_like(src) | ||
|
|
||
| # The wrapper has no allocation of its own. Propagate CUDA allocator | ||
| # stream tracking to every tensor that backs the grouped representation. | ||
| if func == torch.ops.aten.record_stream.default: | ||
| src, stream = args | ||
| if not isinstance(src, GroupedTensor): | ||
| raise TypeError(f"Expected GroupedTensor, got {type(src).__name__}") | ||
| recorded = set() | ||
| for tensor in src.get_data_tensors(): | ||
| if ( | ||
| isinstance(tensor, torch.Tensor) | ||
| and tensor.is_cuda | ||
| and id(tensor) not in recorded | ||
| ): | ||
| tensor.record_stream(stream) | ||
| recorded.add(id(tensor)) | ||
| return None | ||
|
|
||
| # Parameter construction may invoke aten.expand on tensor subclasses. | ||
| # Handle this explicitly so grouped parameters can be created safely. | ||
|
|
@@ -246,7 +307,7 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor: | |
| src_shape[i] if dim == -1 else dim for i, dim in enumerate(expanded_shape) | ||
| ) | ||
| if normalized_shape == src_shape: | ||
| return make_wrapper_like(src, requires_grad=src.requires_grad) | ||
| return make_wrapper_like(src) | ||
| return super().__torch_dispatch__(func, types, args, kwargs) | ||
|
|
||
| # DDP and mcore use expand_as(self) to build a dummy autograd node and | ||
|
|
@@ -259,7 +320,7 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor: | |
| if other is src: | ||
| return _GroupedIdentityFunc.apply(src) | ||
| if tuple(other.shape) == tuple(src.shape): | ||
| return make_wrapper_like(src, requires_grad=src.requires_grad) | ||
| return make_wrapper_like(src) | ||
| return super().__torch_dispatch__(func, types, args, kwargs) | ||
|
|
||
| # Distributed optimizer flattens detached parameters via | ||
|
|
@@ -270,6 +331,14 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor: | |
| if not isinstance(src, GroupedTensor): | ||
| raise TypeError(f"Expected GroupedTensor, got {type(src).__name__}") | ||
| target_shape = tuple(args[1]) | ||
| # Autograd Functions that return an input unchanged create a | ||
| # shape-preserving view to track the output alias. Preserve the | ||
| # grouped layout and share its backing buffers in that case. | ||
| if target_shape == tuple(src.shape): | ||
| # Autograd attaches the differentiable-view metadata after | ||
| # __torch_dispatch__ returns, so the fresh wrapper must not | ||
| # already require gradients. | ||
| return make_wrapper_like(src) | ||
| if target_shape in ((-1,), (src.numel(),)): | ||
| if src.rowwise_data is not None: | ||
| return src.rowwise_data.view(-1) | ||
|
|
@@ -365,6 +434,10 @@ def __torch_function__(cls, func, types, args=(), kwargs=None): | |
| # Do not force GroupedTensor on outputs. | ||
| return torch._C._disabled_torch_function_impl(func, types, args, kwargs) | ||
|
|
||
| def untyped_storage(self) -> torch.UntypedStorage: | ||
| """Return the shared storage facade used for scheduler pseudo-deallocation.""" | ||
| return self._untyped_storage | ||
|
|
||
| def expand_as(self, other: torch.Tensor) -> torch.Tensor: | ||
| # pylint: disable=missing-function-docstring | ||
| # Needed during parameter creation/hook registration paths. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_autograd_identity_supports_varying_shapeschecks thatgrad_fnexists but never runs backward. This PR also changes how alias wrappers setrequires_grad, so the test would miss a failure when gradients pass through the new view. Run backward with an explicit gradient and check that the input receives the expected gradient.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!