Architecture: Autograd & Tape
Architecture: Autograd & Tape
netcl's automatic differentiation engine is a classical reverse-mode tape-based autograd system built around a per-thread Tape. When you wrap execution inside with ag.Tape():, every differentiable operation creates a Node that records:
1. Its output Tensor buffer
2. A grad_fn closure capable of calculating vector-Jacobian products (local gradients)
3. Its parent nodes in the computational graph
Calling tape.backward(loss) walks the computation graph in reverse topological order, evaluating grad_fn closures and accumulating gradients directly onto leaf parameter tensors.
[!TIP] Theory & Curriculum Link: For the first-principles mathematical derivation of the chain rule, reverse-mode automatic differentiation, and computational graphs, see Curriculum: Calculus & Gradients.
The Computational DAG Structure
The forward computation and the reverse gradient propagation are illustrated below for $y = (x + 1)^2, \; z = 3y, \; \text{loss} = \text{sum}(z)$:
Forward edges connect parent nodes to child results. During backward(), netcl traverses reverse edges starting from seed gradient $\frac{\partial \mathcal{L}}{\partial \mathcal{L}} = 1.0$, evaluating each node's grad_fn and accumulating into .grad.
Node: The Graph Record
@dataclass
class Node:
value: Tensor
grad_fn: Optional[GradFn] # Callable[[Tensor], List[Optional[Tensor]]]
parents: List[Node]
grad: Optional[Tensor] # Accumulated gradient
requires_grad: bool
op_name: Optional[str] # For debug prints
creation_trace: Optional[List[str]]
A Node is distinct from a Tensor:
- The Tensor is dumb device memory (cl_mem) with shape and strides.
- The Node is the autograd graph record.
Leaf tensors that do not require gradients have no associated Node. Intermediate tensors whose parents do not require gradients are skipped during graph construction.
Node Fields
value: TheTensorproduced by the forward operation.grad_fn: A closure that takes the upstream gradient and returns local gradients for each parent node.parents: List of input nodes that generated this node.grad: The accumulated gradient tensor flowing back into this node. Leaf parameters store their final gradients here for the optimizer.requires_grad: Boolean flag indicating whether the node participates in backward graph traversal.op_name: Human-readable identifier (e.g."matmul","relu","conv2d").
Tape: The Per-Thread Recorder
class Tape:
def __enter__(self): ...
def __exit__(self, exc_type, exc_val, exc_tb): ...
def backward(self, loss: Node) -> None: ...
The active Tape is bound to the current thread via thread-local storage (threading.local). This provides several guarantees:
- Concurrent worker threads run independent autograd tapes without cross-thread contamination.
- The default context manager provides an implicit active tape consumed by apply_op when no explicit tape= argument is passed.
- Nested tape contexts stack cleanly.
The Tape.backward() Execution Lifecycle
When tape.backward(loss) is invoked:
1. Seed Gradient: Seeds the loss node with $\frac{\partial \mathcal{L}}{\partial \mathcal{L}} = 1.0$ using an OpenCL buffer filled with ones matching loss.value.shape.
2. Reverse Topological Ordering: Builds a reverse topological sort of the graph rooted at loss using Kahn-style depth-first search (build_topo).
3. Kernel Dispatch & Accumulation: Iterates through nodes in reverse order, calling node.grad_fn(node.grad). Local gradients are accumulated into parents using in-place addition (add_inplace), ensuring multi-consumer branch nodes sum incoming gradients correctly.
4. Queue Flush Synchronization: Captures the OpenCL command queue into Tape._pending_flush_queue. Device-to-Host transfers (to_host()) serve as the natural hardware synchronization barrier, avoiding expensive explicit queue.finish() stalls.
Operator Overloading on Nodes
To enable intuitive mathematical syntax, Node implements standard Python dunder methods (__add__, __sub__, __mul__, __matmul__, __pow__):
def __add__(self, other):
from netcl import autograd as ag
return ag.add(self, other)
This allows clean, idiomatic expressions where operations are recorded automatically:
y = (x + 1.0) ** 2
z = 3.0 * y
loss = z.sum()
Memory Management in Autograd
- Retained Intermediate Activations: Every recorded
Nodeholds a reference to its outputTensor. For deep architectures, this dominates VRAM during training. Wrap evaluation and inference routines inwith ag.no_grad():to discard graph nodes immediately. - Anomaly Detection: Wrapping code in
with ag.detect_anomaly():captures Python stack traces at node creation and scans gradient buffers for NaNs and Infs. Use this strictly during debugging, as host roundtrips degrade throughput.
Related Documentation
- Tutorial: Understanding Autograd: Hands-on guide to building custom autograd operations.
- Autograd API Reference: Method signatures for
Tape,Node, and primitives. - Curriculum: Calculus & Gradients: Mathematical derivation of reverse-mode automatic differentiation.