man advect/tutorials/primitives
PRIMITIVES(1)User CommandsPRIMITIVES(1)
────────────────────────────────────────────────────────────────────────────────────────

Custom Primitives

Create a primitive when a numerical operation is opaque to Advect or needs one stable atomic identity in a staged program. Ordinary traceable array code does not need a wrapper; Advect can already see and differentiate its operations.

The cube below is intentionally simple so the authoring contract stays visible. A primitive starts with its concrete implementation, then adds only the rules it promises to support.

import numpy as np

import advect as ad


@ad.primitive
def cube(x):
    return x * x * x


@cube.def_abstract
def cube_abstract(x):
    return x.spec


@cube.def_jvp
def cube_jvp(output, primals, tangents):
    del output
    (x,), (tangent,) = primals, tangents
    if tangent is None:
        return np.zeros_like(x)
    return 3 * x * x * tangent


print("cube:", cube(np.array([1.0, 2.0, 3.0])))

The implementation handles ordinary calls. The abstract rule describes output shape and dtype for staging. The JVP is ordinary traceable code, so Advect can use it for forward mode, derive its transpose for reverse mode, and compose it under higher-order transforms.

Check the promised capabilities

from advect.testing import check_primitive

sample = np.array([1.0, 2.0, 3.0])
check_primitive(
    cube,
    primals=(sample,),
    check=("abstract", "jvp", "transpose", "nested", "stage"),
)

gradient = ad.grad(lambda x: np.sum(cube(x)))(sample)
np.testing.assert_allclose(gradient, 3 * sample**2)
print("gradient:", gradient)

The default check_primitive covers the first-order abstract, JVP, and transpose paths. Request only the extra capabilities the primitive claims, and run materially different shape, dtype, static-argument, and complex cases separately. Follow the primitive check with check_gradient on a representative composition.

The operation name defaults to the function's module and qualified name. Give it an explicit name such as example.cube only when saved programs need an identity independent of the Python module path. Loading still requires the matching implementation to be imported under that name.

Some operations need an explicit transpose, exact forward residuals, static arguments, or intentionally first-order behavior. Those are real contracts, but they are extension-author reference material rather than prerequisites for the common JVP-first path; see the primitive API.

$

[1:docs] [2:playground] $ man advect/tutorials/primitives