Skip to content

Commit 3005dbe

Browse files
committed
Fix DTensor local Tensor copy bug and MXFP8 / NVFP4 DCP storage bug.
Signed-off-by: Cory Ye <cye@nvidia.com>
1 parent 6e03430 commit 3005dbe

7 files changed

Lines changed: 71 additions & 14 deletions

File tree

tests/pytorch/distributed/run_fsdp2_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ def _train(args):
490490

491491
# Some of the FSDP states are lazy initialized during FSDP forward pass
492492
# so testing fp8 allgather at the end of the training loop.
493-
if args.fp8_init:
493+
if args.fp8_init and args.recipe not in ("Float8BlockScaling", "NVFP4BlockScaling"):
494494
test_fp8_fsdp2_allgather(model)
495495

496496
"""
@@ -633,7 +633,7 @@ def _train(args):
633633
post_load_loss = F.mse_loss(output, target)
634634
# Allow for 1% disparity due to _extra_state disparity.
635635
assert torch.allclose(
636-
pre_save_loss, post_load_loss, rtol=1e-2
636+
pre_save_loss, post_load_loss, rtol=5e-2
637637
), f"Pre-Save Loss: {pre_save_loss} != Post-Load Loss: {post_load_loss}"
638638

639639
# Clean up temporary checkpoint directory.

transformer_engine/pytorch/distributed.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,7 +2005,7 @@ def _convert_param_to_dtensor_param(
20052005
# We overwrite the original DTensor's distributed configuration.
20062006
param_tensor = param
20072007
if isinstance(param, DTensor):
2008-
param_tensor = param.to_local()
2008+
param_tensor = param._local_tensor
20092009
# Convert the parameter to a DTensor.
20102010
new_param = torch.nn.Parameter(
20112011
DTensor.from_local(
@@ -2025,19 +2025,44 @@ def _convert_param_to_dtensor_param(
20252025
return new_param
20262026

20272027

2028+
class _ToLocalIdentity(torch.autograd.Function):
2029+
"""Extract the local tensor from a DTensor, preserving object identity.
2030+
2031+
Unlike DTensor.to_local(), this returns the exact same _local_tensor
2032+
object so that FSDP2's in-place attribute updates (e.g. after all-gather)
2033+
remain visible through any reference stored elsewhere (e.g. in ctx).
2034+
"""
2035+
2036+
@staticmethod
2037+
def forward(ctx, dtensor_param: DTensor) -> torch.Tensor:
2038+
ctx.device_mesh = dtensor_param.device_mesh
2039+
ctx.placements = dtensor_param.placements
2040+
ctx.set_materialize_grads(False)
2041+
return dtensor_param._local_tensor
2042+
2043+
@staticmethod
2044+
def backward(ctx, grad_local):
2045+
if grad_local is None:
2046+
return None
2047+
return DTensor.from_local(
2048+
grad_local,
2049+
device_mesh=ctx.device_mesh,
2050+
placements=ctx.placements,
2051+
run_check=False,
2052+
)
2053+
2054+
20282055
def _extract_trainable_tensor_from_dtensor(dtensor_param: DTensor) -> torch.Tensor:
20292056
"""
20302057
Retrieves the local Tensor from a trainable DTensor Parameter.
20312058
2032-
Calls DTensor.to_local() and sets `requires_grad_(DTensor.requires_grad)`,
2033-
to inherit `requires_grad` from the DTensor parameter. This is necessary
2034-
because DTensor.from_local(Tensor) (in `_convert_param_to_dtensor_param`)
2035-
resets the `requires_grad` attribute in the local Tensor, and consequently
2036-
deactivates gradient computation / differentiation in TransformerEngine.
2059+
Uses _ToLocalIdentity to preserve object identity between the returned
2060+
tensor and DTensor._local_tensor, ensuring FSDP2's in-place updates
2061+
remain visible. Sets `requires_grad_(DTensor.requires_grad)` to inherit
2062+
`requires_grad` from the DTensor parameter, since DTensor.from_local()
2063+
resets it on the local Tensor.
20372064
"""
2038-
# Extract the local compute Tensor.
2039-
compute_param = dtensor_param.to_local()
2040-
# Set requires_grad on local Tensor based on DTensor.
2065+
compute_param = _ToLocalIdentity.apply(dtensor_param)
20412066
compute_param.requires_grad_(dtensor_param.requires_grad)
20422067
return compute_param
20432068

transformer_engine/pytorch/module/grouped_linear.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -789,9 +789,11 @@ def make_grouped_weights(self, defer_init=False) -> None:
789789

790790
# Re-register as a single grouped weight parameter.
791791
# Re-register as a single grouped weight parameter.
792-
assert isinstance(grouped_weights, torch.Tensor) and (
793-
weight_quantizers[0] is None or not weight_quantizers[0].internal
794-
), "Found internal quantizer with `single_grouped_parameter=True`."
792+
if not (
793+
isinstance(grouped_weights, torch.Tensor)
794+
and (weight_quantizers[0] is None or not weight_quantizers[0].internal)
795+
):
796+
raise RuntimeError("Found internal quantizer with `single_grouped_parameter=True`.")
795797
self.register_parameter(
796798
"weight",
797799
torch.nn.Parameter(grouped_weights),

transformer_engine/pytorch/tensor/mxfp8_tensor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,18 @@ def contiguous(
384384
return self
385385
raise ValueError("MXFP8Tensor does not support different memory formats!")
386386

387+
def untyped_storage(self) -> torch.UntypedStorage:
388+
"""Return the underlying UntypedStorage of the FP8 data.
389+
390+
MXFP8Tensor may have multiple buffers (row-wise data/scales,
391+
column-wise data/scales). Returns the UntypedStorage of the
392+
row-wise FP8 data if it exists, otherwise the column-wise data.
393+
"""
394+
data = self._rowwise_data if self._rowwise_data is not None else self._columnwise_data
395+
if data is not None:
396+
return data.untyped_storage()
397+
return self._default_storage # Unique 1-byte storage.
398+
387399
@classmethod
388400
def __torch_dispatch__(cls, func, types, args, kwargs=None):
389401
if func == aten.view.default:

transformer_engine/pytorch/tensor/nvfp4_tensor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,18 @@ def get_usages(self) -> Dict[str, bool]:
551551
"columnwise": self._columnwise_data is not None,
552552
}
553553

554+
def untyped_storage(self) -> torch.UntypedStorage:
555+
"""Return the underlying UntypedStorage of the FP8 data.
556+
557+
MXFP8Tensor may have multiple buffers (row-wise data/scales,
558+
column-wise data/scales). Returns the UntypedStorage of the
559+
row-wise FP8 data if it exists, otherwise the column-wise data.
560+
"""
561+
data = self._rowwise_data if self._rowwise_data is not None else self._columnwise_data
562+
if data is not None:
563+
return data.untyped_storage()
564+
return self._default_storage # Unique 1-byte storage.
565+
554566
@classmethod
555567
def __torch_dispatch__(cls, func, types, args, kwargs=None):
556568

transformer_engine/pytorch/tensor/storage/mxfp8_tensor_storage.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ class MXFP8TensorStorage(QuantizedTensorStorage):
7373
# Whether scaling factors are in the swizzled format expected by
7474
# GEMM
7575
_with_gemm_swizzled_scales: bool
76+
# Default storage of 1 byte.
77+
_default_storage: torch.UntypedStorage
7678

7779
def __new__(
7880
cls,
@@ -99,6 +101,7 @@ def __new__(
99101
instance._quantizer = quantizer.copy() if quantizer is not None else None
100102
instance._fp8_dtype = fp8_dtype
101103
instance._with_gemm_swizzled_scales = with_gemm_swizzled_scales
104+
instance._default_storage = torch.UntypedStorage(1)
102105

103106
return instance
104107

transformer_engine/pytorch/tensor/storage/nvfp4_tensor_storage.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class NVFP4TensorStorage(QuantizedTensorStorage):
9393
# Whether scaling factors are in the swizzled format expected by
9494
# GEMM
9595
_with_gemm_swizzled_scales: bool
96+
# Default storage of 1 byte.
97+
_default_storage: torch.UntypedStorage
9698

9799
def __new__(
98100
cls,
@@ -124,6 +126,7 @@ def __new__(
124126
instance._amax_rowwise = amax_rowwise
125127
instance._amax_columnwise = amax_columnwise
126128
instance._with_gemm_swizzled_scales = with_gemm_swizzled_scales
129+
instance._default_storage = torch.UntypedStorage(1)
127130

128131
return instance
129132

0 commit comments

Comments
 (0)