๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Custom Autograd Functions

A custom autograd function is a class that supplies its own forward and backward rules. Autograd calls the backward rule during backward() and trusts the gradients it returns.

The built-in clamp keeps its own rule

The built-in clamp caps the value at 1.0 here. Its fixed backward rule passes no gradient outside the range.

import torch

x = torch.tensor([2.0], requires_grad=True)
y = x.clamp(-1.0, 1.0)
print("Clamped value:", y.item())
y.backward()
print("Gradient outside the range:", x.grad.item())

A first custom function

DoubleFn doubles its input in forward. Its backward doubles the incoming gradient, because the derivative of two times x is two.

class DoubleFn(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        return x * 2

    @staticmethod
    def backward(ctx, grad_output):
        return grad_output * 2


x = torch.tensor([3.0], requires_grad=True)
y = DoubleFn.apply(x)
print("Forward:", y.tolist())
y.backward()
print("Gradient:", x.grad.tolist())

Save the input for backward

The square rule needs the input again inside backward. save_for_backward stores it, and backward reads it back from ctx.

class SquareFn(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        ctx.save_for_backward(x)
        return x * x

    @staticmethod
    def backward(ctx, grad_output):
        (x,) = ctx.saved_tensors
        return grad_output * 2 * x


x = torch.tensor([4.0], requires_grad=True)
y = SquareFn.apply(x)
y.backward()
print("Forward:", y.item())
print("Gradient:", x.grad.item())

Example

import torch


class ClampFn(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, low, high):
        ctx.save_for_backward(x)
        ctx.low = low
        ctx.high = high
        return x.clamp(low, high)

    @staticmethod
    def backward(ctx, grad_output):
        (x,) = ctx.saved_tensors
        inside = (x >= ctx.low) & (x <= ctx.high)
        return grad_output * inside.to(grad_output.dtype), None, None


x = torch.tensor([-2.0, -0.5, 0.3, 2.0], requires_grad=True)
y = ClampFn.apply(x, -1.0, 1.0)
print("Input:", [round(v, 2) for v in x.detach().tolist()])
print("Clamped:", [round(v, 2) for v in y.tolist()])
y.sum().backward()
print("Gradient:", x.grad.tolist())

Key points

  • forward: the method that computes the output from the inputs
  • backward: the method that turns an incoming gradient into gradients for the inputs
  • ctx: the context object that carries saved values from forward to backward
  • Use save_for_backward on ctx to keep tensors that backward needs
  • Call a custom function through its apply method, never by calling the class
  • In short, forward does the maths and backward does the derivative, both written by hand

In this example, ClampFn is a custom autograd function with two saved pieces of state. Its forward clamps the input into the range from minus one to one. It saves the input on ctx and stores the two bounds beside it. Its backward builds a mask of the inputs inside the range. It returns the incoming gradient times that mask, plus None for each bound. The input row holds minus two, minus a half, 0.3, and two. The clamped row prints the two outside values pulled to the bounds. After sum and backward, the gradient prints one inside the range and zero outside. The two None returns match the two bound arguments, which need no gradient.

Note

A wrong backward still runs, and only a numerical check can expose it

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

What must backward return for an input that needs no gradient, like a bound?