AD Architecture
Repos: computegraph-rs, tenferro-rs Parent: ../index.md Related: computegraph.md, primitive-ad.md, semantic-ad.md, ../spec/ad-contract.md, ../spec/backend-contract.md, ../spec/primitive-catalog.md
Purpose
This document describes the current end-to-end AD pipeline after U8. The active implementation is tenferro-owned: tenferro-ad transforms semantic programs, tenferro-internal-ops owns core primitive rules, and operation-family crates register semantic extension rules.
The design model remains linearize-then-transpose:
linearizeis the derivative-producing step.transpose_rulereverses already-linear flow.- compilation and execution happen after the derivative program has been built and validated.
Current Traced Pipeline
Traced tensor workflows build a backend-neutral program first:
TracedTensor operations
-> TraceContext
-> GraphCompiler
-> CompiledGraph / FrozenProgram
Functional AD transforms then operate on that frozen semantic program:
JVP:
FrozenProgram
-> semantic_jvp_with_cache
-> SemanticAdProgram
-> GraphCompiler / Runtime
-> Tensor outputs
VJP:
FrozenProgram
-> semantic_vjp_with_cache
-> SemanticAdProgram
-> GraphCompiler / Runtime
-> cotangent Tensor outputs
The semantic transform preserves validation and source metadata. It does not execute tensor buffers and does not choose backend kernels. Backend execution starts only when the resulting program is compiled and handed to Runtime.
Current Eager Pipeline
Eager operations execute immediately, but eager AD still records enough semantic information to reuse the same rule model:
EagerTensor operation
-> execute concrete operation
-> record operation metadata for AD
-> use AdContext / EagerRuntime transform cache when a functional AD
transform or backward pass needs derivative programs
Stateful eager reverse mode (backward and backward_with) additionally accumulates gradients into tracked leaf slots. Functional eager transforms (grad, vjp, and jvp) return tensors and do not mutate gradient slots.
Rule Dispatch
Core primitive rules are registered by PrimitiveOpKind in tenferro-internal-ops/src/ad/registry.rs. The core rule entry points are:
PrimitiveAdRule::linearize
PrimitiveAdRule::transpose_rule
Extension rules are registered by operation-family crates in a SemanticExtensionRuleSet. tenferro-ad owns lookup and dispatch for semantic extension rules. Missing rules must report unsupported AD; they must not drop tangent or cotangent flow.
Direct primal VJP rules are allowed only as explicit escape hatches. The default route for reverse-mode support is:
linearize -> transpose_rule -> optimize -> compile -> execute
Shape And Output Activity
Before a transform emits derivative operations, tenferro computes which primal outputs and inputs are active. That information is carried through ShapeGuardContext and semantic metadata so multi-output rules can avoid building unused derivative branches.
For example, an SVD whose caller only consumes singular values should be able to emit the singular-value derivative path without also building the vector F-matrix chain. materialize_merge, semantic validation, and compiler passes still deduplicate later, but rule emission should avoid known-dead work early.
Rules must distinguish:
- rank only;
- exact static extents;
- conservative or unknown extents;
- runtime shape sources.
Do not require exact extents when rank or runtime shape references are enough.
Symbolic Zero And Constants
None in a tangent or cotangent slot means symbolic zero flow. Rules keep that state symbolic until another primitive requires a real tensor value. At that boundary, rules instantiate zeros through dtype-aware semantic helpers.
The same discipline applies to constants, one-like tensors, and identity matrices. AD rules should use helpers such as constant_scalar, zero_like, one_like, and identity_matrix, not analytic operations such as exp, log, or sin, to manufacture constants.
Cache Ownership
AD transform caching is owner-scoped:
AdContextowns extension rules and a bounded transform cache.EagerRuntimeinstances created with anAdContextshare that cache handle.- directly constructed eager runtimes own private caches.
- direct traced helper methods remain stateless unless the caller supplies an
AdContext.
Cached AD entries store graph/program artifacts only. They must not retain tensor buffers, backend allocations, or concrete execution outputs. Cache keys must be deterministic and metadata-only: semantic fingerprints, input metadata, active masks, requested output slots, and alias information.
Scalar Example
For f(x) = exp(a * x), the traced program first records the primal:
p0 = Input(x)
p1 = Input(a)
p2 = Mul(p0, p1)
p3 = Exp(p2)
JVP with respect to x emits linear flow equivalent to:
t0 = Input(dx)
t1 = Mul(a, t0)
t2 = Mul(exp(a*x), t1)
VJP transposes that linear flow:
c0 = Input(dy)
c1 = Mul(exp(a*x), c0)
c2 = Mul(a, c1)
The generated program is then compiled and executed forward like any other semantic program.
Reduction Example
For f(x) = sum(x), linearization emits the same reduction on tangent flow:
dy = sum(dx)
Reverse mode transposes that linear map by broadcasting the scalar cotangent to the input shape:
dx_bar = broadcast_in_dim(dy_bar, shape(x))
This example is why transpose rules must keep shape metadata available without forcing tensor execution during rule emission.
Complex Example
For a holomorphic elementwise function y = f(z), forward mode treats the map as real-linear:
dy = f'(z) * dz
Reverse mode transposes under the real inner product:
dz_bar = y_bar * conj(f'(z))
The normative complex convention lives in ../spec/ad-contract.md.
Golden Invariants
AD implementation and review should preserve these invariants:
- derivative programs are validation-preserving
Program -> Programtransforms; - rule emission is deterministic for payload, metadata, active masks, context, and extension rule set;
- active-output pruning happens before large multi-output derivative branches are emitted;
- explicit cotangent accumulation remains visible as graph/program operations;
- cache entries are bounded, clearable, introspectable, and owner-scoped;
- numerical oracle or finite-difference coverage is the correctness gate for supported AD rules.
Historical design notes and migration worklogs may still mention older engine names. Active architecture and spec pages should use the current tenferro-owned interfaces documented here.