Autograd OpSpecs (spec layer) #
This file defines small OpSpec building blocks (forward + VJP) for common tensor operations.
The definitions are intentionally direct mathematical contracts and live purely in the spec layer.
The declarations follow a uniform pattern:
- Each operation below is an
OpSpec: a pureforwardplus a pure VJPbackward. - Most ops here package
*Specand derivative-spec definitions fromNN/Spec/*.
Where this sits in TorchLean:
NN.Spec.*files define pure denotational semantics: what tensors/layers mean.- This file packages some of those pure definitions as unary
OpSpecs:forwardplus VJP. NN.Runtime.Autograd.*executes programs, tracks parameters, manages tapes/sessions, dispatches CUDA kernels, handles RNG, and lowers graphs for reusable typed execution.
This file adapts operations whose input-gradient VJP is naturally expressed as a single OpSpec.
Larger multi-input or parameterized layers (convolution, attention, batchnorm, pooling, RNG)
still have precise specs and runtime implementations, but their full backward state usually belongs
in layer/runtime code rather than in this compact unary interface.
PyTorch analogy (approximately):
- A spec
OpSpecis like a compacttorch.autograd.Functionwhere we write down the VJP directly. - We do not model PyTorch's mutable
ctx; the spec layer receives the input tensorxdirectly.
Elementwise lifting helpers #
Lift an elementwise backward using the chain rule: $\frac{\partial L}{\partial x}=f'(x)\frac{\partial L}{\partial y}$ pointwise.
This is the standard VJP pattern for elementwise ops.
PyTorch analogy: the "local backward" rule for a pointwise op multiplies by the derivative mask.
Instances For
Elementwise ReLU OpSpec on any shape.
PyTorch analogy: torch.relu(x) / torch.nn.functional.relu(x).
Instances For
Elementwise sigmoid OpSpec on any shape.
PyTorch analogy: torch.sigmoid(x).
Instances For
Elementwise tanh OpSpec on any shape.
PyTorch analogy: torch.tanh(x).
Instances For
Elementwise softplus OpSpec on any shape.
PyTorch analogy: torch.nn.functional.softplus(x).
Instances For
Elementwise SiLU (also called Swish) OpSpec on any shape.
PyTorch analogy: torch.nn.functional.silu(x).
Instances For
Elementwise ELU OpSpec on any shape.
Instances For
Elementwise tanh-approximate GELU OpSpec on any shape.
PyTorch analogy: torch.nn.functional.gelu(x, approximate="tanh").
Instances For
Elementwise hyperbolic sine OpSpec.
Instances For
Elementwise hyperbolic cosine OpSpec.
Instances For
Softmax OpSpec along an explicitly selected tensor dimension.
PyTorch analogy: torch.softmax(x, dim=axis).
Instances For
Stable log-softmax OpSpec along an explicitly selected tensor dimension.
Backward recomputes the forward output so the VJP uses the same axis-parametric semantics. Runtime engines may cache that output instead.
Instances For
Linear layers #
Linear layer as an OpSpec: $y=Wx+b$.
This OpSpec only returns the input gradient $\partial L/\partial x$. Parameter gradients for
$W$ and $b$
are not part of OpSpec (those live at the graph/runtime level).
PyTorch analogy: torch.nn.functional.linear forward, with autograd producing gradients for
x, W, and b.
Instances For
Generic elementwise binary OpSpec with captured right-hand tensor and d/dx.
This is a "closure style" op: we treat the RHS tensor as a captured constant and only return the VJP with respect to the LHS input.
PyTorch analogy: in a tape/graph, rhs is typically another node; here we are writing the
"lhs-only" derivative for convenience.
Instances For
Scale by constant scalar.
PyTorch analogy: x * c where c is a scalar constant.
Instances For
Unary elementwise ops #
Negation (-x).
Instances For
Absolute value (uses signSpec for the subgradient).
PyTorch analogy: torch.abs(x). At $x=0$ we pick the subgradient $0$.
Instances For
Smooth absolute value (a differentiable surrogate for abs).
This is useful when you want to avoid a kink at 0 in optimization.
PyTorch analogy: there is no single canonical smoothAbs, but it is similar in spirit to
$\sqrt{x^2+\varepsilon}$-style smoothings.
Instances For
Elementwise exp.
PyTorch analogy: torch.exp(x).
Instances For
Elementwise natural logarithm.
Domain discipline: this is the raw mathematical/PyTorch-style rule. The VJP divides by x,
so callers should use it only when the input is strictly positive. Runtime backends are allowed to
reject nonpositive inputs rather than silently manufacture a gradient. Use safeLogOp when the
intended model is the smooth surrogate $\log(\operatorname{softplus}(x)+\varepsilon)$.
PyTorch analogy: torch.log(x).
Instances For
Elementwise smooth logarithm surrogate, $\log(\operatorname{softplus}(x)+\varepsilon)$.
For $\varepsilon>0$ this is defined on every real input. Its VJP multiplies by $\operatorname{sigmoid}(x)/(\operatorname{softplus}(x)+\varepsilon)$.
PyTorch expression: torch.log(torch.nn.functional.softplus(x) + eps).
Instances For
Elementwise square root.
The forward pass evaluates sqrt (max x 0). The backward pass uses the local factor
$1/(2\sqrt{x})$ when x > 0, and zero otherwise, then multiplies that factor by the upstream
value. Checked scalar backends replay the complete weighted quotient if an intermediate loses
range, using the root rounded by that backend. Over the reals this gives a zero VJP for
$x\le0$. On IEEE backends, a zero factor
multiplied by an infinite or NaN upstream value need not produce zero.
PyTorch analogy: torch.sqrt(x) on the positive region, with an explicit TorchLean subgradient
choice outside the classical domain.
Instances For
Elementwise square, $x^2$.
Instances For
Elementwise power with a captured RHS exponent tensor.
This is the VJP with respect to the base $x$ for $x^{\mathtt{rhs}}$. Domain restrictions are the usual ones for the scalar backend's power operation. The zero-exponent branch tests all differentiation coefficients, so a zero exponent primal does not erase mixed derivatives. For a constant exponent, checked range loss refines the complete weighted derivative instead of rounding an unrepresentable local factor before applying the incoming gradient.
Instances For
Elementwise reciprocal, $1/x$.
Domain discipline: this is the raw reciprocal. Its VJP is $-1/x^2$, so callers should use it only
when zero is excluded by the surrounding invariant. Use safeInvOp when the intended model is
$1/(x+\varepsilon)$.
PyTorch analogy: torch.reciprocal(x) or 1 / x.
Instances For
Elementwise epsilon-shifted reciprocal, $1/(x+\varepsilon)$.
The forward pass delegates to safedivSpec with unit numerator, and the VJP uses the derivative
of the same shifted expression. The shift alone does not exclude a zero denominator: over
the reals, x = -Context.defaultEpsilon makes it zero.
PyTorch analogy: usually written manually as 1.0 / (x + eps).
Instances For
Binary ops capturing a right-hand tensor #
Add a captured RHS tensor, $x+\mathtt{rhs}$.
Instances For
Subtract a captured RHS tensor, $x-\mathtt{rhs}$.
Instances For
Elementwise multiply by a captured RHS tensor.
Instances For
Elementwise divide by a captured RHS tensor.
Domain discipline: this is the raw division rule. The VJP multiplies by 1/rhs, so callers should
only use it when the captured denominator is known nonzero. Use safeDivOp when the
intended model is x/(rhs+ε).
Instances For
Elementwise epsilon-shifted division by a captured RHS tensor, $x/(\mathtt{rhs}+\varepsilon)$. The shift does not guarantee a nonzero denominator or finite output.
PyTorch analogy: usually written manually as x / (rhs + eps).
Instances For
Elementwise minimum with a captured right-hand tensor.
The backward pass multiplies the upstream value by a mask: one where the input is strictly smaller
than rhs, zero where it is strictly larger, and half when neither comparison holds. The last
case includes ties and unordered IEEE comparisons. This is the same mask as the two-input tape
operation. Capturing rhs removes its gradient output; it does not transfer its half of a tied
gradient to the remaining input. A zero mask can still produce NaN with a nonfinite upstream value.
Away from ties this is the usual derivative. At a tie, minimum is not differentiable, and the
half-gradient is the convention used by the runtime.
The half is a rational constant, so constructing it does not require a representable 2.
Instances For
Elementwise maximum with a captured right-hand tensor.
The backward pass multiplies the upstream value by a mask: one where the input is strictly larger
than rhs, zero where it is strictly smaller, and half when neither comparison holds. As in
minOp, the captured tensor keeps its share of the selected gradient even though this operation
returns only the gradient with respect to the input.
The strict comparisons and their order match the two-input tape operation, including its fallback when neither comparison holds.
Instances For
Leaky ReLU with slope parameter.
PyTorch analogy: torch.nn.functional.leaky_relu(x, negative_slope=alpha_l).
Instances For
Clamp OpSpec with a fixed interval.
We choose the standard subgradient 1 strictly inside the interval and 0 at/outside the
boundaries, matching clampDerivativeSpec.
Instances For
Loss OpSpecs #
MSE loss (returns a scalar), capturing the target.
Instances For
MAE loss (returns a scalar), capturing the target.
Instances For
Huber loss (returns a scalar), capturing the target.
Instances For
Cross-entropy loss (returns a scalar), capturing the target distribution.
This is "cross-entropy between distributions": target is $p$, yhat is $q$.
PyTorch analogy: closer to -(p * log(q)).mean() than to the logits-based
torch.nn.functional.cross_entropy default.
Instances For
Logits-based cross-entropy loss, capturing the target distribution.
Instances For
Binary cross-entropy loss on probability tensors, capturing the target tensor.
Instances For
Cosine-similarity loss, capturing the target tensor.
Instances For
Hinge loss (returns a scalar), capturing the target.
Instances For
Poisson loss (returns a scalar), capturing the target.
Instances For
Log-cosh loss (returns a scalar), capturing the target.
Instances For
Shape/structure ops #
Reshape op (requires a size-equality proof).
PyTorch analogy: x.reshape(...) (or view), but here the shape relationship is explicit.
Instances For
Swap adjacent axes at an arbitrary depth.
Instances For
Fill a tensor with a constant (ignores input).
PyTorch analogy: torch.full_like(x, value) (but here we keep the input only to fit the OpSpec
shape, and ignore its content).
Instances For
Replicate a scalar to any shape; backward sums gradients back to a scalar.
PyTorch analogy: broadcasting a scalar in arithmetic, and in backward accumulating by sum.
Instances For
Apply boolean mask: keep where mask true, else set 0.
PyTorch analogy: torch.where(mask, x, 0).
Instances For
Masked inverted-dropout OpSpec with an explicit mask.
Instances For
Matrix-rank multiplication with a captured right operand and broadcasted batch prefixes.
Instances For
Matrix-rank multiplication with a captured left operand and broadcasted batch prefixes.
Instances For
One-hot embedding as an OpSpec over the one-hot input. Parameter gradients stay outside
OpSpec; this wrapper returns only dOneHot.
Instances For
Slice a contiguous range along an arbitrary tensor axis.
Instances For
Reductions and broadcasting #
Reduce-sum along axis using a NonemptyAxis proof; backward broadcasts back.
PyTorch analogy: torch.sum(x, dim=axis) (with keepdim=false).
Instances For
Generic broadcasting-aware binary OpSpec.
The caller supplies:
- explicit broadcast proofs (
CanBroadcastTo) for both sides, and - a
reduceBackmap that takes a gradient in the broadcasted shapetand reduces it back to the left shapes1.
PyTorch analogy: this is where PyTorch's implicit broadcasting rules and reduction-of-broadcasted gradients ("sum over broadcasted dimensions") happen. In TorchLean we keep those shape relations explicit.