Skip to content

Commit d9152b0

Browse files
[PyTorch] Support GroupedTensor torch ops for DDP and distributed optimizer (NVIDIA#2736)
* Fix e2e execution of GroupedTensor in distributed settings Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * Minor fixes Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * fix Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * Update transformer_engine/pytorch/tensor/storage/grouped_tensor_storage.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * fix greptile commit Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> --------- Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 145e88c commit d9152b0

3 files changed

Lines changed: 263 additions & 50 deletions

File tree

transformer_engine/pytorch/csrc/quantizer.cpp

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,11 @@ std::pair<GroupedTensorWrapper, py::object> NoneQuantizer::create_grouped_tensor
180180
py::handle GroupedTensorClass = grouped_tensor_python_class(this->internal);
181181
py::dict kwargs;
182182
py::tuple args(0);
183-
kwargs["shape"] = py::cast(std::vector<int64_t>{static_cast<int64_t>(logical_first_dim),
184-
static_cast<int64_t>(logical_last_dim)});
183+
const std::vector<int64_t> grouped_shape = {static_cast<int64_t>(logical_first_dim),
184+
static_cast<int64_t>(logical_last_dim)};
185+
const std::vector<int64_t> grouped_stride = stride_from_shape(grouped_shape);
186+
kwargs["shape"] = py::cast(grouped_shape);
187+
kwargs["stride"] = py::cast(grouped_stride);
185188
kwargs["dtype"] = py::cast(GetATenDType(dtype));
186189
kwargs["num_tensors"] = py::cast(num_tensors);
187190
kwargs["quantizer"] = quantizer;
@@ -386,8 +389,11 @@ std::pair<GroupedTensorWrapper, py::object> Float8Quantizer::create_grouped_tens
386389
py::handle GroupedTensorClass = grouped_tensor_python_class(this->internal);
387390
py::dict kwargs;
388391
py::tuple args(0);
389-
kwargs["shape"] = py::cast(std::vector<int64_t>{static_cast<int64_t>(logical_first_dim),
390-
static_cast<int64_t>(logical_last_dim)});
392+
const std::vector<int64_t> grouped_shape = {static_cast<int64_t>(logical_first_dim),
393+
static_cast<int64_t>(logical_last_dim)};
394+
const std::vector<int64_t> grouped_stride = stride_from_shape(grouped_shape);
395+
kwargs["shape"] = py::cast(grouped_shape);
396+
kwargs["stride"] = py::cast(grouped_stride);
391397
kwargs["dtype"] = py::cast(GetATenDType(dtype));
392398
kwargs["num_tensors"] = py::cast(num_tensors);
393399
kwargs["quantizer"] = quantizer;
@@ -704,8 +710,11 @@ std::pair<GroupedTensorWrapper, py::object> Float8CurrentScalingQuantizer::creat
704710
py::handle GroupedTensorClass = grouped_tensor_python_class(this->internal);
705711
py::dict kwargs;
706712
py::tuple args(0);
707-
kwargs["shape"] = py::cast(std::vector<int64_t>{static_cast<int64_t>(logical_first_dim),
708-
static_cast<int64_t>(logical_last_dim)});
713+
const std::vector<int64_t> grouped_shape = {static_cast<int64_t>(logical_first_dim),
714+
static_cast<int64_t>(logical_last_dim)};
715+
const std::vector<int64_t> grouped_stride = stride_from_shape(grouped_shape);
716+
kwargs["shape"] = py::cast(grouped_shape);
717+
kwargs["stride"] = py::cast(grouped_stride);
709718
kwargs["dtype"] = py::cast(GetATenDType(dtype));
710719
kwargs["num_tensors"] = py::cast(num_tensors);
711720
kwargs["quantizer"] = quantizer;
@@ -1062,8 +1071,11 @@ std::pair<GroupedTensorWrapper, py::object> Float8BlockQuantizer::create_grouped
10621071
py::handle GroupedTensorClass = grouped_tensor_python_class(this->internal);
10631072
py::dict kwargs;
10641073
py::tuple args(0);
1065-
kwargs["shape"] = py::cast(std::vector<int64_t>{static_cast<int64_t>(logical_first_dim),
1066-
static_cast<int64_t>(logical_last_dim)});
1074+
const std::vector<int64_t> grouped_shape = {static_cast<int64_t>(logical_first_dim),
1075+
static_cast<int64_t>(logical_last_dim)};
1076+
const std::vector<int64_t> grouped_stride = stride_from_shape(grouped_shape);
1077+
kwargs["shape"] = py::cast(grouped_shape);
1078+
kwargs["stride"] = py::cast(grouped_stride);
10671079
kwargs["dtype"] = py::cast(GetATenDType(dtype));
10681080
kwargs["num_tensors"] = py::cast(num_tensors);
10691081
kwargs["quantizer"] = quantizer;
@@ -1478,8 +1490,11 @@ std::pair<GroupedTensorWrapper, py::object> MXFP8Quantizer::create_grouped_tenso
14781490
py::handle GroupedTensorClass = grouped_tensor_python_class(this->internal);
14791491
py::dict kwargs;
14801492
py::tuple args(0);
1481-
kwargs["shape"] = py::cast(std::vector<int64_t>{static_cast<int64_t>(logical_first_dim),
1482-
static_cast<int64_t>(logical_last_dim)});
1493+
const std::vector<int64_t> grouped_shape = {static_cast<int64_t>(logical_first_dim),
1494+
static_cast<int64_t>(logical_last_dim)};
1495+
const std::vector<int64_t> grouped_stride = stride_from_shape(grouped_shape);
1496+
kwargs["shape"] = py::cast(grouped_shape);
1497+
kwargs["stride"] = py::cast(grouped_stride);
14831498
kwargs["dtype"] = py::cast(GetATenDType(dtype));
14841499
kwargs["num_tensors"] = py::cast(num_tensors);
14851500
kwargs["quantizer"] = quantizer;
@@ -1906,8 +1921,11 @@ std::pair<GroupedTensorWrapper, py::object> NVFP4Quantizer::create_grouped_tenso
19061921
py::handle GroupedTensorClass = grouped_tensor_python_class(this->internal);
19071922
py::dict kwargs;
19081923
py::tuple args(0);
1909-
kwargs["shape"] = py::cast(std::vector<int64_t>{static_cast<int64_t>(logical_first_dim),
1910-
static_cast<int64_t>(logical_last_dim)});
1924+
const std::vector<int64_t> grouped_shape = {static_cast<int64_t>(logical_first_dim),
1925+
static_cast<int64_t>(logical_last_dim)};
1926+
const std::vector<int64_t> grouped_stride = stride_from_shape(grouped_shape);
1927+
kwargs["shape"] = py::cast(grouped_shape);
1928+
kwargs["stride"] = py::cast(grouped_stride);
19111929
kwargs["dtype"] = py::cast(GetATenDType(dtype));
19121930
kwargs["num_tensors"] = py::cast(num_tensors);
19131931
kwargs["quantizer"] = quantizer;

transformer_engine/pytorch/tensor/grouped_tensor.py

Lines changed: 153 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,36 @@
1414
from .storage.grouped_tensor_storage import GroupedTensorStorage
1515

1616

17-
# For now, conservatively ban all shape manipulating ops.
17+
def _stride_from_shape(shape: Tuple[int, ...]) -> Tuple[int, ...]:
18+
"""Calculate contiguous stride from shape."""
19+
if len(shape) == 0:
20+
return ()
21+
stride = [1] * len(shape)
22+
for i in range(len(shape) - 2, -1, -1):
23+
stride[i] = stride[i + 1] * shape[i + 1]
24+
return tuple(stride)
25+
26+
27+
class _GroupedIdentityFunc(torch.autograd.Function):
28+
"""Identity autograd function used to create a dummy grad_fn node."""
29+
30+
@staticmethod
31+
def forward(ctx, tensor: "GroupedTensor") -> "GroupedTensor":
32+
# pylint: disable=missing-function-docstring
33+
ctx.input_dtype = tensor.dtype
34+
return tensor.detach()
35+
36+
@staticmethod
37+
def backward(ctx, grad_output: torch.Tensor):
38+
# pylint: disable=missing-function-docstring
39+
grad_input = grad_output
40+
if grad_input.dtype != ctx.input_dtype:
41+
grad_input = grad_input.to(ctx.input_dtype)
42+
return grad_input
43+
44+
45+
# For now, conservatively ban 'most' shape manipulating ops.
1846
BANNED_SHAPE_OPS = {
19-
torch.ops.aten.view.default,
20-
torch.ops.aten._unsafe_view.default,
2147
torch.ops.aten.reshape.default,
2248
torch.ops.aten._reshape_alias.default,
2349
torch.ops.aten.flatten.using_ints,
@@ -34,8 +60,6 @@
3460
torch.ops.aten.select.int,
3561
torch.ops.aten.split.Tensor,
3662
torch.ops.aten.chunk.default,
37-
torch.ops.aten.expand.default,
38-
torch.ops.aten.expand_as.default,
3963
torch.ops.aten.cat.default,
4064
torch.ops.aten.stack.default,
4165
}
@@ -48,6 +72,7 @@ def __new__(
4872
cls,
4973
shape: Tuple[int, int],
5074
dtype: torch.dtype,
75+
*,
5176
num_tensors: int,
5277
shapes: Optional[List[Tuple[int, int]]] = None,
5378
quantizer: Optional[Quantizer] = None,
@@ -64,12 +89,9 @@ def __new__(
6489
offsets: Optional[List[int]] = None,
6590
scale_inv_offsets: Optional[List[int]] = None,
6691
columnwise_scale_inv_offsets: Optional[List[int]] = None,
92+
requires_grad: bool = False,
93+
stride: Optional[List[int]] = None,
6794
):
68-
del quantizer
69-
del offsets
70-
del scale_inv_offsets
71-
del columnwise_scale_inv_offsets
72-
7395
if (
7496
shapes is not None
7597
and len(shapes) == num_tensors
@@ -99,29 +121,136 @@ def __new__(
99121
if device is None:
100122
device = torch.device("cuda")
101123

102-
strides = [1] * len(wrapper_shape)
103-
for i in range(len(wrapper_shape) - 2, -1, -1):
104-
strides[i] = strides[i + 1] * wrapper_shape[i + 1]
105-
return torch.Tensor._make_wrapper_subclass(
124+
# Match QuantizedTensor __new__: accept externally-computed stride to
125+
# avoid Python-side stride computation overhead for C++ construction.
126+
strides = _stride_from_shape(tuple(wrapper_shape)) if stride is None else tuple(stride)
127+
instance = torch.Tensor._make_wrapper_subclass(
106128
cls,
107129
wrapper_shape,
108-
strides=tuple(strides),
130+
strides=strides,
109131
storage_offset=0,
110132
dtype=dtype,
111133
layout=torch.strided,
112-
requires_grad=False,
134+
requires_grad=requires_grad,
113135
device=device,
114136
)
137+
GroupedTensorStorage._initialize_storage_fields(
138+
instance=instance,
139+
shape=shape,
140+
dtype=dtype,
141+
num_tensors=num_tensors,
142+
shapes=shapes,
143+
quantizer=quantizer,
144+
data=data,
145+
columnwise_data=columnwise_data,
146+
scale_inv=scale_inv,
147+
columnwise_scale_inv=columnwise_scale_inv,
148+
amax=amax,
149+
columnwise_amax=columnwise_amax,
150+
scale=scale,
151+
first_dims=first_dims,
152+
last_dims=last_dims,
153+
tensor_offsets=tensor_offsets,
154+
offsets=offsets,
155+
scale_inv_offsets=scale_inv_offsets,
156+
columnwise_scale_inv_offsets=columnwise_scale_inv_offsets,
157+
)
158+
return instance
115159

116160
@classmethod
117161
def __torch_dispatch__(cls, func, types, args, kwargs=None):
118162
"""Dispatch by dequantizing grouped members, then requantizing writes."""
119163
if kwargs is None:
120164
kwargs = {}
121165

166+
def copy_grouped_storage_metadata(dst: GroupedTensor, src: GroupedTensor) -> None:
167+
"""Shallow-copy grouped-storage metadata onto wrapper outputs."""
168+
dst.num_tensors = src.num_tensors
169+
dst.quantizer = src.quantizer
170+
dst.tensor_shapes = src.tensor_shapes
171+
dst.fake_dtype = src.fake_dtype
172+
dst.rowwise_data = src.rowwise_data
173+
dst.columnwise_data = src.columnwise_data
174+
dst.scale_inv = src.scale_inv
175+
dst.columnwise_scale_inv = src.columnwise_scale_inv
176+
dst.amax = src.amax
177+
dst.columnwise_amax = src.columnwise_amax
178+
dst.scale = src.scale
179+
dst.first_dims = src.first_dims
180+
dst.last_dims = src.last_dims
181+
dst.tensor_offsets = src.tensor_offsets
182+
dst.offsets = src.offsets
183+
dst.scale_inv_offsets = src.scale_inv_offsets
184+
dst.columnwise_scale_inv_offsets = src.columnwise_scale_inv_offsets
185+
dst.logical_shape = src.logical_shape
186+
dst.quantized_tensors = src.quantized_tensors
187+
188+
def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
189+
"""Create a wrapper of the same type and tensor metadata as src."""
190+
out = torch.Tensor._make_wrapper_subclass(
191+
type(src),
192+
tuple(src.shape),
193+
strides=tuple(src.stride()),
194+
storage_offset=src.storage_offset(),
195+
dtype=src.dtype,
196+
layout=src.layout,
197+
requires_grad=requires_grad,
198+
device=src.device,
199+
)
200+
copy_grouped_storage_metadata(out, src)
201+
return out
202+
122203
# Parameter construction calls detach()/alias-like paths.
123204
if func in (torch.ops.aten.detach.default, torch.ops.aten.alias.default):
124-
return args[0]
205+
src = args[0]
206+
assert isinstance(src, GroupedTensor)
207+
if func == torch.ops.aten.detach.default:
208+
return make_wrapper_like(src, requires_grad=False)
209+
return make_wrapper_like(src, requires_grad=src.requires_grad)
210+
211+
# Parameter construction may invoke aten.expand on tensor subclasses.
212+
# Handle this explicitly so grouped parameters can be created safely.
213+
if func == torch.ops.aten.expand.default:
214+
src = args[0]
215+
assert isinstance(src, GroupedTensor)
216+
expanded_shape = tuple(args[1])
217+
src_shape = tuple(src.shape)
218+
if len(expanded_shape) == len(src_shape):
219+
normalized_shape = tuple(
220+
src_shape[i] if dim == -1 else dim for i, dim in enumerate(expanded_shape)
221+
)
222+
if normalized_shape == src_shape:
223+
return make_wrapper_like(src, requires_grad=src.requires_grad)
224+
return super().__torch_dispatch__(func, types, args, kwargs)
225+
226+
# DDP and mcore use expand_as(self) to build a dummy autograd node and
227+
# access gradient accumulators during parameter hook registration.
228+
if func == torch.ops.aten.expand_as.default:
229+
src = args[0]
230+
other = args[1]
231+
assert isinstance(src, GroupedTensor)
232+
if other is src:
233+
return _GroupedIdentityFunc.apply(src)
234+
if tuple(other.shape) == tuple(src.shape):
235+
return make_wrapper_like(src, requires_grad=src.requires_grad)
236+
return super().__torch_dispatch__(func, types, args, kwargs)
237+
238+
# Distributed optimizer flattens detached parameters via
239+
# model_param.detach().view(-1). Support this path explicitly by
240+
# returning a flat view of grouped backing storage.
241+
if func in (torch.ops.aten.view.default, torch.ops.aten._unsafe_view.default):
242+
src = args[0]
243+
assert isinstance(src, GroupedTensor)
244+
target_shape = tuple(args[1])
245+
if target_shape in ((-1,), (src.numel(),)):
246+
if src.rowwise_data is not None:
247+
return src.rowwise_data.view(-1)
248+
raise RuntimeError(
249+
f"{cls.__name__} view(-1) requires rowwise_data to be initialized"
250+
)
251+
raise RuntimeError(
252+
f"{cls.__name__} only supports view(-1) for distributed optimizer flattening"
253+
)
125254

126255
# Don't allow reshape/view etc.
127256
if func in BANNED_SHAPE_OPS:
@@ -203,3 +332,10 @@ def __torch_function__(cls, func, types, args=(), kwargs=None):
203332
kwargs = {}
204333
# Do not force GroupedTensor on outputs.
205334
return torch._C._disabled_torch_function_impl(func, types, args, kwargs)
335+
336+
def expand_as(self, other: torch.Tensor) -> torch.Tensor:
337+
# pylint: disable=missing-function-docstring
338+
# Needed during parameter creation/hook registration paths.
339+
if other is self:
340+
return _GroupedIdentityFunc.apply(self)
341+
return super().expand_as(other)

0 commit comments

Comments
 (0)