Skip to content

Commit 2199b71

Browse files
authored
Support GroupedTensor autograd views and stream tracking (#3701)
Preserve autograd view metadata for GroupedTensor wrappers and record streams on their backing tensors. Add focused coverage for gradient propagation through an identity view and for stream tracking. Signed-off-by: Varun Thumbe <vthumbe@nvidia.com>
1 parent c9c9225 commit 2199b71

2 files changed

Lines changed: 94 additions & 13 deletions

File tree

‎tests/pytorch/test_grouped_tensor.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,76 @@ def setup_class(cls) -> None:
299299
torch.manual_seed(seed)
300300
torch.cuda.manual_seed(seed)
301301

302+
def test_autograd_identity_view_preserves_grouped_storage_and_gradient(self) -> None:
303+
"""Autograd's identity view keeps grouped metadata and passes back a dense gradient."""
304+
305+
class Identity(torch.autograd.Function):
306+
@staticmethod
307+
def forward(ctx, tensor):
308+
return tensor
309+
310+
@staticmethod
311+
def backward(ctx, grad):
312+
return grad
313+
314+
first_dims = torch.tensor([1, 2], dtype=torch.int64, device="cuda")
315+
tensor_offsets = torch.tensor([0, 4, 12], dtype=torch.int64, device="cuda")
316+
source = GroupedTensor(
317+
shape=(3, 4),
318+
dtype=torch.float32,
319+
num_tensors=2,
320+
data=torch.randn(12, device="cuda"),
321+
first_dims=first_dims,
322+
tensor_offsets=tensor_offsets,
323+
requires_grad=True,
324+
)
325+
326+
view = Identity.apply(source)
327+
assert isinstance(view, GroupedTensor)
328+
assert view is not source
329+
assert view._base is source
330+
assert view.rowwise_data is source.rowwise_data
331+
assert view.first_dims is first_dims
332+
assert view.tensor_offsets is tensor_offsets
333+
334+
expected = torch.arange(1, 13, dtype=torch.float32, device="cuda").reshape(3, 4)
335+
(grad,) = torch.autograd.grad(view, source, grad_outputs=expected)
336+
assert type(grad) is torch.Tensor
337+
torch.testing.assert_close(grad, expected)
338+
339+
def test_record_stream_tracks_grouped_buffers(self, monkeypatch) -> None:
340+
"""record_stream should reach every allocation owned by the wrapper."""
341+
first_dims = torch.tensor([1, 2], dtype=torch.int64, device="cuda")
342+
last_dims = torch.tensor([4, 4], dtype=torch.int64, device="cuda")
343+
tensor_offsets = torch.tensor([0, 4, 12], dtype=torch.int64, device="cuda")
344+
grouped_tensor = GroupedTensor(
345+
shape=(3, 4),
346+
dtype=torch.float32,
347+
num_tensors=2,
348+
data=torch.randn(12, device="cuda"),
349+
columnwise_data=torch.randn(12, device="cuda"),
350+
scale_inv=torch.ones(2, device="cuda"),
351+
columnwise_scale_inv=torch.ones(2, device="cuda"),
352+
amax=torch.zeros(2, device="cuda"),
353+
columnwise_amax=torch.zeros(2, device="cuda"),
354+
scale=torch.ones(2, device="cuda"),
355+
first_dims=first_dims,
356+
last_dims=last_dims,
357+
tensor_offsets=tensor_offsets,
358+
)
359+
buffers = grouped_tensor.get_data_tensors()
360+
recorded = []
361+
original_record_stream = torch.Tensor.record_stream
362+
363+
def record_stream(tensor, stream):
364+
recorded.append(tensor)
365+
return original_record_stream(tensor, stream)
366+
367+
monkeypatch.setattr(torch.Tensor, "record_stream", record_stream)
368+
grouped_tensor.record_stream(torch.cuda.Stream())
369+
370+
assert all(any(record is buffer for record in recorded) for buffer in buffers)
371+
302372
def test_basic_construction_all_same_shape(self) -> None:
303373
"""Test GroupedTensor construction with all tensors having same shape"""
304374
num_tensors = 4

‎transformer_engine/pytorch/tensor/grouped_tensor.py‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def copy_grouped_storage_metadata(dst: GroupedTensor, src: GroupedTensor) -> Non
209209
dst.nvfp4_e4m3_max = src.nvfp4_e4m3_max
210210
dst.scale_inv_dtype = src._scale_inv_dtype
211211

212-
def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
212+
def make_wrapper_like(src: GroupedTensor) -> GroupedTensor:
213213
"""Create a wrapper of the same type and tensor metadata as src."""
214214
out = torch.Tensor._make_wrapper_subclass(
215215
type(src),
@@ -218,7 +218,9 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
218218
storage_offset=src.storage_offset(),
219219
dtype=src.dtype,
220220
layout=src.layout,
221-
requires_grad=requires_grad,
221+
# Autograd attaches the output's differentiable-view metadata after
222+
# __torch_dispatch__ returns.
223+
requires_grad=False,
222224
device=src.device,
223225
)
224226
copy_grouped_storage_metadata(out, src)
@@ -229,9 +231,18 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
229231
src = args[0]
230232
if not isinstance(src, GroupedTensor):
231233
raise TypeError(f"Expected GroupedTensor, got {type(src).__name__}")
232-
if func == torch.ops.aten.detach.default:
233-
return make_wrapper_like(src, requires_grad=False)
234-
return make_wrapper_like(src, requires_grad=src.requires_grad)
234+
return make_wrapper_like(src)
235+
236+
# The wrapper has no allocation of its own. Propagate CUDA allocator
237+
# stream tracking to every tensor that backs the grouped representation.
238+
if func == torch.ops.aten.record_stream.default:
239+
src, stream = args
240+
if not isinstance(src, GroupedTensor):
241+
raise TypeError(f"Expected GroupedTensor, got {type(src).__name__}")
242+
for tensor in src.get_data_tensors():
243+
if tensor is not None and tensor.is_cuda:
244+
tensor.record_stream(stream)
245+
return None
235246

236247
# Parameter construction may invoke aten.expand on tensor subclasses.
237248
# Handle this explicitly so grouped parameters can be created safely.
@@ -246,7 +257,7 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
246257
src_shape[i] if dim == -1 else dim for i, dim in enumerate(expanded_shape)
247258
)
248259
if normalized_shape == src_shape:
249-
return make_wrapper_like(src, requires_grad=src.requires_grad)
260+
return make_wrapper_like(src)
250261
return super().__torch_dispatch__(func, types, args, kwargs)
251262

252263
# DDP and mcore use expand_as(self) to build a dummy autograd node and
@@ -259,7 +270,7 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
259270
if other is src:
260271
return _GroupedIdentityFunc.apply(src)
261272
if tuple(other.shape) == tuple(src.shape):
262-
return make_wrapper_like(src, requires_grad=src.requires_grad)
273+
return make_wrapper_like(src)
263274
return super().__torch_dispatch__(func, types, args, kwargs)
264275

265276
# Distributed optimizer flattens detached parameters via
@@ -270,6 +281,11 @@ def make_wrapper_like(src: GroupedTensor, requires_grad: bool) -> GroupedTensor:
270281
if not isinstance(src, GroupedTensor):
271282
raise TypeError(f"Expected GroupedTensor, got {type(src).__name__}")
272283
target_shape = tuple(args[1])
284+
# Autograd Functions that return an input unchanged create a
285+
# shape-preserving view to track the output alias. Preserve the
286+
# grouped layout and share its backing buffers in that case.
287+
if target_shape == tuple(src.shape):
288+
return make_wrapper_like(src)
273289
if target_shape in ((-1,), (src.numel(),)):
274290
if src.rowwise_data is not None:
275291
return src.rowwise_data.view(-1)
@@ -358,12 +374,7 @@ def maybe_update_inplace(arg, new_arg, schema_arg):
358374
new_kwargs = tree_map(maybe_unwrap, kwargs)
359375
return super().__torch_dispatch__(func, types, new_args, new_kwargs)
360376

361-
@classmethod
362-
def __torch_function__(cls, func, types, args=(), kwargs=None):
363-
if kwargs is None:
364-
kwargs = {}
365-
# Do not force GroupedTensor on outputs.
366-
return torch._C._disabled_torch_function_impl(func, types, args, kwargs)
377+
__torch_function__ = torch._C._disabled_torch_function_impl
367378

368379
def expand_as(self, other: torch.Tensor) -> torch.Tensor:
369380
# pylint: disable=missing-function-docstring

0 commit comments

Comments
 (0)