The dispatcher and custom ops

PyTorch · Expert · 18 min

Custom kernels (Triton, CUDA, C++) only integrate cleanly with compile, export and autograd through the operator registry.

Every torch.* call ends up in the dispatcher, which routes an operator such as aten::add to an implementation based on dispatch keys computed from the inputs: Autograd, then backend keys like CPU, CUDA or Meta, plus features like Autocast or Python subclasses. That is how one operator gets autograd, mixed precision and device-specific kernels without the caller knowing.

Python can hook in at two levels:

  • __torch_function__ on a Tensor subclass intercepts calls at the Python API level.
  • __torch_dispatch__ intercepts after autograd, at the ATen operator level; it is how fake tensors, functionalisation and DTensor are built.

To add your own operator, use `torch.library.custom_op`. It registers the op with a schema inferred from type hints, and you add:

  • register_fake: a shape-and-dtype-only implementation, so torch.compile and torch.export can trace through the op without running it.
  • register_autograd(backward, setup_context=...): the derivative.
  • torch.library.opcheck(op, args): tests that the registrations are consistent.

The implementation can call anything: a Triton kernel, a C++ extension, even NumPy. Custom ops must not return views of their inputs, and they appear as single opaque nodes in compiled graphs. For C++ and CUDA kernels, torch.utils.cpp_extension builds them and TORCH_LIBRARY registers them.

Example

import torch
from torch import Tensor

@torch.library.custom_op("mylib::scaled_tanh", mutates_args=())
def scaled_tanh(x: Tensor, scale: float) -> Tensor:
    return torch.tanh(x) * scale        # could call Triton, C++ or NumPy here

@scaled_tanh.register_fake
def _(x, scale):
    return torch.empty_like(x)          # shapes/dtypes only, used while tracing

def setup_context(ctx, inputs, output):
    _, scale = inputs
    ctx.save_for_backward(output)
    ctx.scale = scale

def backward(ctx, grad):
    (y,) = ctx.saved_tensors
    return grad * ctx.scale * (1 - (y / ctx.scale) ** 2), None

scaled_tanh.register_autograd(backward, setup_context=setup_context)

x = torch.randn(4, requires_grad=True)
torch.ops.mylib.scaled_tanh(x, 2.0).sum().backward()
print(x.grad.shape)                     # torch.Size([4])

Remember

  • The dispatcher routes ops by keys: Autograd, Autocast, CPU, CUDA, Meta...
  • torch.library.custom_op + register_fake + register_autograd
  • Test registrations with torch.library.opcheck

Watch out for

  • Skipping register_fake, which makes torch.compile graph-break or fail on the op.

Check yourself

  1. What is a fake (meta) implementation of a custom op used for?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pytorch.org/docs/stable/library.html

Builds on

Unlocks

Nothing builds on this yet.