Rate this Page

Source code for monarch.gradient_generator

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# pyre-unsafe
import math
from contextlib import nullcontext
from functools import partial
from types import CellType, FunctionType
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union

import torch
import torch.autograd.graph
from monarch.common import device_mesh, stream
from monarch.common.tensor import Tensor
from monarch.common.tree import flatten
from monarch.gradient import GradientGenerator as _GradientGenerator
from torch._C._autograd import _get_sequence_nr  # @manual
from torch.autograd.graph import get_gradient_edge, GradientEdge

TensorOrEdge = Union[torch.Tensor, GradientEdge]


class Context(NamedTuple):
    device_mesh: "Optional[device_mesh.DeviceMesh]"
    stream: "stream.Stream"

    def enable(self):
        if device_mesh is None:
            activate_mesh = device_mesh.no_mesh.activate()
        elif self.device_mesh is not device_mesh._active:
            # XXX: something about activating device meshes from this object
            # doesn't work correctly and somehow inactivates the device mesh
            # if it is already enabled. This is a temporary workaround for
            # the demo.
            # pyrefly: ignore [missing-attribute]
            activate_mesh = self.device_mesh.activate()
        else:
            activate_mesh = nullcontext()
        with activate_mesh, self.stream.activate(), torch.no_grad():
            yield


_sequence_nr_to_context: Dict[int, Context] = {}
_sequence_nr_end = 0


def restore_context(t: Optional[Tensor], sn: Optional[int], last: bool):
    if sn is not None:
        _update_context_map(Context(device_mesh._active, stream._active))
        ctx = _sequence_nr_to_context.pop(sn) if last else _sequence_nr_to_context[sn]
        return ctx.enable()
    if t is not None:
        return Context(t.mesh, t.stream).enable()
    return Context(device_mesh._active, stream._active).enable()


def _update_context_map(ctx: Context):
    global _sequence_nr_end
    next_sequence_nr = _get_sequence_nr()
    for i in range(_sequence_nr_end, next_sequence_nr):
        _sequence_nr_to_context[i] = ctx
    _sequence_nr_end = _get_sequence_nr()


device_mesh._on_change.append(
    lambda old, mesh: _update_context_map(Context(old, stream._active))
)
stream._on_change.append(
    lambda old, stream: _update_context_map(Context(device_mesh._active, old))
)


[docs]def grad_generator( roots: Union[torch.Tensor, Sequence[TensorOrEdge]] = (), with_respect_to: Sequence[TensorOrEdge] = (), grad_roots: Sequence[Optional[torch.Tensor]] = (), ): if isinstance(roots, torch.Tensor): roots = [roots] return _GradientGenerator( list(roots), list(with_respect_to), list(grad_roots), restore_context )
def _gradient_edge(a: TensorOrEdge) -> GradientEdge: if isinstance(a, GradientEdge): return a return get_gradient_edge(a) class GradGenerator: def __init__(self): self.roots: List[torch.autograd.graph.GradientEdge] = [] self.with_respect_to: List[torch.autograd.graph.GradientEdge] = [] self.grad_roots: List[Optional[torch.Tensor]] = [] self.unflattens: List[Tuple[int, Any]] = [] def grad(self, tree: Any): tensors, unflatten = flatten(tree, lambda x: isinstance(x, torch.Tensor)) self.unflattens.append((len(tensors), unflatten)) self.with_respect_to.extend(_gradient_edge(t) for t in tensors) def root(self, r: TensorOrEdge, grad: Optional[torch.Tensor] = None): self.roots.append(_gradient_edge(r)) self.grad_roots.append(grad) def __iter__(self): gi = _GradientGenerator( self.roots, list(reversed(self.with_respect_to)), self.grad_roots, restore_context, ) for n, unflatten in reversed(self.unflattens): yield unflatten(reversed([next(gi) for _ in range(n)])) class GradFunction(torch.autograd.Function): @staticmethod def forward(ctx, fn, *args, **kwargs): result, backward_continuation = fn(*args, **kwargs) ctx.backward_continuation = backward_continuation values = [] if backward_continuation.__closure__ is not None: for cell in backward_continuation.__closure__: values.append(cell.cell_contents) cell.cell_contents = None tensors, ctx.unflatten = flatten(values, lambda x: isinstance(x, torch.Tensor)) ctx.save_for_backward(*tensors) return result @staticmethod def backward(ctx, *args, **kwargs): closure = tuple(CellType(v) for v in ctx.unflatten(ctx.saved_tensors)) orig = ctx.backward_continuation fn = FunctionType( orig.__code__, orig.__globals__, orig.__name__, orig.__defaults__, closure ) output = fn(*args, **kwargs) if isinstance(output, tuple): return None, *output else: return None, output
[docs]def grad_function(fn): return partial(GradFunction.apply, fn)
def gradient_execution_order( roots: Sequence[TensorOrEdge], with_respect_to: Sequence[Any] ) -> List[int]: """ Returns the order in which the gradients for `with_respect_to` would become available if autograd were run on `roots`. This is the reverse order of each tensors first use in the gradient computation. """ with_respect_to = [ (g.node, g.output_nr) for g in map(_gradient_edge, with_respect_to) ] min_sequence_nr: Dict[Any, float] = {e: math.inf for e in with_respect_to} to_scan = [_gradient_edge(r).node for r in roots] scanned = set() while to_scan: node = to_scan.pop() if node in scanned: continue scanned.add(node) for key in node.next_functions: nnode = key[0] if nnode is None: continue to_scan.append(nnode) value = min_sequence_nr.get(key) if value is not None: min_sequence_nr[key] = min(node._sequence_nr(), value) return sorted( range(len(with_respect_to)), key=lambda i: min_sequence_nr[with_respect_to[i]], reverse=True, )