Needed for straight-through estimators, numerically stable gradients, and wrapping non-PyTorch code.
When autograd's automatic derivative is not what you want, subclass torch.autograd.Function and write both directions yourself:
forward(ctx, *inputs): compute the output. Store what backward needs withctx.save_for_backward(*tensors); non-tensor values can be set as attributes onctx.backward(ctx, *grad_outputs): receive the gradient of the loss with respect to each output and return one gradient per forward input, orNonefor inputs that need none (like integer arguments).
Both methods are @staticmethods, and you call the function with MyFn.apply(x), never by instantiating it. Recent PyTorch versions also support splitting out a setup_context(ctx, inputs, output) method, which is required for compatibility with torch.func transforms.
Typical uses:
- Straight-through estimators: round or binarise in the forward pass but pretend it was identity in the backward pass, as in quantisation-aware training.
- Stable or cheaper gradients for a fused formula.
- Wrapping code autograd cannot see, such as a NumPy or C++ routine.
Always check a hand-written backward with torch.autograd.gradcheck, which compares it to finite differences. Use float64 inputs, because float32 is too imprecise for the check.
Example
import torch
from torch.autograd import Function
class MyExp(Function):
@staticmethod
def forward(ctx, x):
y = x.exp()
ctx.save_for_backward(y) # exp's derivative is exp itself
return y
@staticmethod
def backward(ctx, grad_out):
(y,) = ctx.saved_tensors
return grad_out * y
class RoundSTE(Function):
@staticmethod
def forward(ctx, x):
return x.round()
@staticmethod
def backward(ctx, grad_out):
return grad_out # pretend round() was identity
x = torch.randn(3, dtype=torch.double, requires_grad=True)
print(torch.autograd.gradcheck(MyExp.apply, (x,))) # True
z = torch.tensor([0.4, 1.6], requires_grad=True)
RoundSTE.apply(z).sum().backward()
print(z.grad) # tensor([1., 1.]) (true derivative is 0)Remember
- Static
forward/backward; call withFn.apply(...) - Return one gradient per input,
Nonewhere not needed - Verify with
gradcheckon float64
Watch out for
- Returning fewer gradients from backward than forward had inputs, which raises an error.