Custom Gradients¶
Sometimes Tangent cannot, or should not, derive a gradient by transforming a function's body: the function calls into a C extension or external solver, you want a numerically better rule than the one differentiation would produce, or you need to attach a gradient to a library op Tangent does not already know.
Tangent gives you two documented ways to supply the rule yourself. Pick by who owns the function:
| You want to… | Use |
|---|---|
| Give your own function a custom rule, or wrap a black box you call | custom_vjp |
Attach a rule to a library op you do not own (numpy.hypot, a backend function) |
register_adjoint / register_tangent |
custom_vjp: your own functions and black boxes¶
Decorate the function with @tangent.custom_vjp, then register its
reverse-mode rule with .defvjp. The rule is plain Python — no template
DSL — and receives the incoming cotangent g, the primal output ans, and
then the primal arguments; it returns one gradient per argument:
import numpy as np
import tangent
@tangent.custom_vjp
def gelu(x):
return 0.5 * x * (1.0 + np.tanh(0.7978845608 * (x + 0.044715 * x ** 3)))
@gelu.defvjp
def gelu_vjp(g, ans, x): # (cotangent, primal output, *primal args)
cdf = 0.5 * (1.0 + np.tanh(0.7978845608 * (x + 0.044715 * x ** 3)))
pdf = np.exp(-0.5 * x * x) * 0.3989422804
return g * (cdf + x * pdf) # one gradient (unary function)
grad_gelu = tangent.grad(gelu)
For a function of several arguments, return a tuple of gradients, one per argument, in order.
Wrapping a black box¶
Because Tangent never transforms the body of a custom_vjp function — it calls
it as-is in the primal and defers to your rule in reverse — the body may do
anything, including call code Tangent cannot see:
import scipy.optimize
import tangent
@tangent.custom_vjp
def solve(a, b):
# opaque to Tangent: a C/Fortran routine with no Python source to transform
return scipy.optimize.brentq(lambda t: a * t * t - b, 0.0, 10.0)
@solve.defvjp
def solve_vjp(g, ans, a, b):
# implicit-function theorem: d(root)/d(params) from the residual
dr_dt = 2.0 * a * ans
return (g * (-ans * ans / dr_dt), g * (1.0 / dr_dt))
Forward mode¶
Register the optional forward-mode rule with .defjvp. It receives the primal
output ans, the primal arguments, then the argument tangents, and returns the
output tangent:
@gelu.defjvp
def gelu_jvp(ans, x, dx): # (primal output, *primal args, *arg tangents)
cdf = 0.5 * (1.0 + np.tanh(0.7978845608 * (x + 0.044715 * x ** 3)))
pdf = np.exp(-0.5 * x * x) * 0.3989422804
return dx * (cdf + x * pdf)
custom_vjp functions must have a fixed positional signature (no
*args/**kwargs).
register_adjoint: library ops you don't own¶
When the function is one you cannot decorate — numpy.hypot, a backend op —
attach a rule to the existing callable with tangent.register_adjoint (reverse
mode) and, optionally, tangent.register_tangent (forward mode). These use
Tangent's template DSL: the decorated function is parsed, not executed, so
its body is written in terms of d[...] derivative markers.
import numpy as np
import tangent
@tangent.register_adjoint(np.hypot)
def hypot_adjoint(z, x, y): # first param: primal output; rest: inputs
d[x] = d[z] * x / z # assign each input's gradient from d[output]
d[y] = d[z] * y / z
@tangent.register_tangent(np.hypot)
def hypot_tangent(z, x, y):
d[z] = (x * d[x] + y * d[y]) / z
def norm(x, y):
return np.hypot(x, y)
tangent.grad(norm, wrt=(0, 1))(3.0, 4.0) # -> (0.6, 0.8)
Inside a template, d[v] is the derivative associated with variable v, and
the parameter names bind to the differentiated call's output and arguments in
order. A registered rule shadows any built-in rule for that op. Like any
function Tangent differentiates, a template must have retrievable source —
define it in a module, not the REPL.
These are the same low-level decorators (tangent.grads.adjoint /
tangent.tangents.tangent_) that Tangent's own backend extensions use to
register the built-in NumPy/JAX/PyTorch rules; register_adjoint and
register_tangent are their public, documented spelling.
Freezing part of a computation¶
tangent.stop_gradient(x) is the identity in the primal and has a zero
derivative in both modes — the standard way to treat part of a computation as a
constant: