latest (dev)
Copy
Latest development documentation · Updated 2026-10-15
Source code for tensorplay.autograd
"""
``tensorplay.autograd`` provides classes and functions implementing automatic differentiation of arbitrary scalar valued functions.
It requires minimal changes to the existing code - you only need to declare :class:`Tensor` s
for which gradients should be computed with the ``requires_grad=True`` keyword.
As of now, we only support autograd for floating point :class:`Tensor` types (
half, float, double and bfloat16) and complex :class:`Tensor` types (cfloat, cdouble).
"""
import warnings
from collections.abc import Mapping, Sequence
from typing import Optional, Union
import tensorplay
from tensorplay.types import _size, _TensorOrTensors, _TensorOrTensorsOrGradEdge
from .grad_mode import (
enable_grad,
inference_mode,
no_grad,
set_grad_enabled,
is_grad_enabled,
_unsafe_preserve_version_counter,
)
from .function import Function, NestedIOFunction
from .variable import Variable
from .graph import save_on_cpu, saved_tensor_hooks as saved_tensors_hooks
from . import forward_ad
from .._C._autograd import (
backward as _backward,
grad as _grad,
is_anomaly_enabled,
is_anomaly_check_nan_enabled,
is_inference_mode_enabled,
)
__all__ = [
"Function",
"Variable",
"NestedIOFunction",
"backward",
"grad",
"grad_mode",
"enable_grad",
"is_grad_enabled",
"inference_mode",
"no_grad",
"set_grad_enabled",
"saved_tensors_hooks",
"save_on_cpu",
# inference mode
"is_inference_mode_enabled",
# anomaly mode
"detect_anomaly",
"set_detect_anomaly",
"is_anomaly_enabled",
"is_anomaly_check_nan_enabled",
# functional API for higher-order derivatives
"jacobian",
"hessian",
"vjp",
"vhp",
"hvp",
"jvp",
]
_OptionalTensor = Optional[tensorplay.Tensor]
_ShapeorNestedShape = Union[_size, Sequence[_size], tensorplay.Tensor]
def _as_tuple(value, length=None):
if value is None:
return (None,) * length if length is not None else None
if isinstance(value, tensorplay.Tensor):
return (value,)
return tuple(value)
def _make_grads(outputs, grads, is_grads_batched=False):
if len(outputs) != len(grads):
raise RuntimeError(
"The number of grad_outputs must match the number of outputs"
)
result = []
for index, (output, grad_output) in enumerate(zip(outputs, grads)):
if not isinstance(output, tensorplay.Tensor):
raise TypeError("outputs must contain tensorplay.Tensor values")
if grad_output is None:
if output.requires_grad:
if output.numel() != 1:
raise RuntimeError(
"grad can be implicitly created only for scalar outputs"
)
if not output.dtype.is_floating_point:
raise RuntimeError(
"grad can be implicitly created only for real scalar outputs"
)
result.append(tensorplay.ones_like(output))
continue
if not isinstance(grad_output, tensorplay.Tensor):
raise TypeError(
"gradients can be either Tensors or None, but got "
+ type(grad_output).__name__
)
grad_shape = tuple(grad_output.shape)
if is_grads_batched:
grad_shape = grad_shape[1:]
if grad_shape != tuple(output.shape):
if is_grads_batched:
raise RuntimeError(
"If `is_grads_batched=True`, we interpret the first "
"dimension of each grad_output as the batch dimension. "
"The sizes of the remaining dimensions are expected to match "
f"the shape of corresponding output, but a mismatch was "
f"detected: grad_output[{index}] has a shape of "
f"{grad_shape} and output[{index}] has a shape of "
f"{tuple(output.shape)}. If you only want some tensors in "
"`grad_output` to be considered batched, consider using vmap."
)
raise RuntimeError(
f"Mismatch in shape: grad_output[{index}] has a shape of "
f"{grad_output.shape} and output[{index}] has a shape of "
f"{output.shape}."
)
if output.dtype.is_complex != grad_output.dtype.is_complex:
raise RuntimeError(
"For complex Tensors, both grad_output and output are "
"required to have the same dtype category."
)
result.append(grad_output)
return tuple(result)
def _inputs_tuple(inputs, caller):
"""The tensors named by ``inputs``: one tensor, a sequence, or a dict's values."""
if isinstance(inputs, tensorplay.Tensor):
return (inputs,)
if type(inputs) is dict:
return tuple(inputs.values())
if isinstance(inputs, Mapping):
raise TypeError(
f"`inputs` argument to `{caller}()` must be a dict, not "
f"{type(inputs).__name__}. Other Mapping types are not supported."
)
return tuple(inputs)
[docs]
def backward(
tensors: _TensorOrTensorsOrGradEdge,
grad_tensors: Optional[_TensorOrTensors] = None,
retain_graph: Optional[bool] = None,
create_graph: bool = False,
grad_variables: Optional[_TensorOrTensors] = None,
inputs: Optional[_TensorOrTensorsOrGradEdge] = None,
) -> None:
r"""Compute the sum of gradients of given tensors with respect to graph leaves.
The gradients are accumulated in the leaves' ``.grad``. If any of
``tensors`` is non-scalar and requires gradient, ``grad_tensors`` gives
the "vector" of the vector-Jacobian product for each of them (``None``
for scalars and tensors that do not need one).
Args:
tensors (Sequence[Tensor] or Tensor): Tensors whose derivative is computed.
grad_tensors (Sequence[Tensor or None] or Tensor, optional): The
"vector" in the vector-Jacobian product, usually gradients w.r.t.
each element of corresponding tensors.
retain_graph (bool, optional): If ``False``, the graph used to compute
the grad will be freed. Defaults to the value of ``create_graph``.
create_graph (bool, optional): If ``True``, the graph of the derivative
is constructed, allowing higher order derivative products.
grad_variables (Sequence[Tensor or None] or Tensor, optional): Deprecated
spelling of ``grad_tensors``.
inputs (Sequence[Tensor] or Tensor or dict, optional): Tensors w.r.t.
which the gradient will be accumulated into ``.grad``. All other
tensors are ignored, and only the part of the graph that leads to
these runs. A non-leaf named here keeps its gradient as if it
had called :meth:`~tensorplay.Tensor.retain_grad`. A dict (e.g.
``dict(model.named_parameters())``) names its values.
"""
if grad_variables is not None:
warnings.warn(
"`grad_variables` is deprecated. Use `grad_tensors` instead.",
FutureWarning,
stacklevel=2,
)
if grad_tensors is None:
grad_tensors = grad_variables
else:
raise RuntimeError(
"`grad_tensors` and `grad_variables` (deprecated) "
"arguments both passed to `backward()`. Please only "
"use `grad_tensors`."
)
inputs_tuple = None
if inputs is not None:
inputs_tuple = _inputs_tuple(inputs, "backward")
if len(inputs_tuple) == 0:
raise RuntimeError("`inputs` argument to `backward()` cannot be empty.")
outputs = _as_tuple(tensors)
grad_tuple = _as_tuple(grad_tensors, len(outputs))
grad_tuple = _make_grads(outputs, grad_tuple)
if retain_graph is None:
retain_graph = create_graph
_backward(outputs, grad_tuple, retain_graph, create_graph, inputs_tuple)
[docs]
def grad(
outputs: _TensorOrTensorsOrGradEdge,
inputs: _TensorOrTensorsOrGradEdge,
grad_outputs: Optional[_TensorOrTensors] = None,
retain_graph: Optional[bool] = None,
create_graph: bool = False,
only_inputs: bool = True,
allow_unused: Optional[bool] = None,
is_grads_batched: bool = False,
materialize_grads: bool = False,
):
r"""Compute and return the sum of gradients of outputs with respect to the inputs.
``grad_outputs`` should be a sequence of length matching ``output``
containing the "vector" in vector-Jacobian product, usually the pre-computed
gradients w.r.t. each of the outputs. If an output doesn't require_grad,
then the gradient can be ``None``).
.. note::
If you run any forward ops, create ``grad_outputs``, and/or call ``grad``
in a user-specified CUDA stream context, see
:ref:`Stream semantics of backward passes<bwd-cuda-stream-semantics>`.
Args:
outputs (sequence of Tensor or GradientEdge): outputs of the differentiated function.
inputs (sequence of Tensor or GradientEdge): Inputs w.r.t. which the gradient will be
returned (and not accumulated into ``.grad``).
grad_outputs (sequence of Tensor): The "vector" in the vector-Jacobian product.
Usually gradients w.r.t. each output. None values can be specified for scalar
Tensors or ones that don't require grad. If a None value would be acceptable
for all grad_tensors, then this argument is optional. Default: None.
retain_graph (bool, optional): If ``False``, the graph used to compute the grad
will be freed. Note that in nearly all cases setting this option to ``True``
is not needed and often can be worked around in a much more efficient
way. Defaults to the value of ``create_graph``.
create_graph (bool, optional): If ``True``, graph of the derivative will
be constructed, allowing to compute higher order derivative products.
Default: ``False``.
only_inputs (bool, optional): Deprecated and ignored; gradients are
only ever returned for ``inputs``.
allow_unused (Optional[bool], optional): If ``False``, specifying inputs
that were not used when computing outputs (and therefore their grad is
always zero) is an error. Defaults to the value of ``materialize_grads``.
is_grads_batched (bool, optional): If ``True``, the first dimension of
each tensor in ``grad_outputs`` is a batch dimension: one
vector-Jacobian product is computed per entry, in a single
:func:`tensorplay.vmap` over the backward pass. Default: ``False``.
materialize_grads (bool, optional): If ``True``, the gradient of an
unused input is returned as zeros instead of ``None``. Default:
``False``.
Returns a tuple with one gradient per input, or a dict keyed like
``inputs`` when ``inputs`` is a dict.
"""
if materialize_grads and allow_unused is False:
raise ValueError(
"Expected allow_unused to be True or not passed when materialize_grads=True, "
"but got: allow_unused=False."
)
if allow_unused is None:
allow_unused = materialize_grads
if not only_inputs:
warnings.warn(
"only_inputs argument is deprecated and is ignored now "
"(defaults to True). To accumulate gradient for other "
"parts of the graph, please use tensorplay.autograd.backward.",
FutureWarning,
stacklevel=2,
)
named = inputs if type(inputs) is dict else None
outputs = _as_tuple(outputs)
inputs_tuple = _inputs_tuple(inputs, "grad")
if len(inputs_tuple) == 0:
raise RuntimeError("grad requires non-empty inputs.")
if retain_graph is None:
retain_graph = create_graph
grad_outputs = _make_grads(
outputs, _as_tuple(grad_outputs, len(outputs)), is_grads_batched
)
if is_grads_batched:
# One vector-Jacobian product per entry of the batch dimension. An
# unused input has no gradient in any of them; vmap cannot return
# None, so its slot is filled while batching and emptied after.
unused: set[int] = set()
def vjp(*batch):
got = _grad(outputs, inputs_tuple, batch, retain_graph, create_graph, allow_unused)
filled = []
for index, (g, inp) in enumerate(zip(got, inputs_tuple)):
if g is None:
unused.add(index)
g = tensorplay.zeros_like(inp)
filled.append(g)
return tuple(filled)
result = tensorplay.vmap(vjp)(*grad_outputs)
result = tuple(None if i in unused else r for i, r in enumerate(result))
else:
result = _grad(outputs, inputs_tuple, grad_outputs, retain_graph, create_graph, allow_unused)
if materialize_grads:
result = tuple(
r if r is not None else tensorplay.zeros_like(inp, requires_grad=create_graph)
for r, inp in zip(result, inputs_tuple)
)
if named is not None:
return dict(zip(named.keys(), result))
return result
from .anomaly_mode import detect_anomaly, set_detect_anomaly # noqa: E402
from . import functional as functional # noqa: E402
from .functional import jacobian, hessian, vjp, vhp, hvp, jvp # noqa: E402
from .gradcheck import ( # noqa: E402
gradcheck,
gradgradcheck,
GradcheckError,
)
from . import profiler as profiler # noqa: E402
from .profiler import emit_nvtx as emit_nvtx # noqa: E402
from . import profiler_legacy as profiler_legacy # noqa: E402Help improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

