DL0068 Computational Graphs

What is a computational graph, and why is it useful in deep learning frameworks?

Answer

A computational graph is a directed acyclic graph whose nodes are operations or stored values and whose edges are the tensors flowing between them. Deep learning frameworks materialize this graph while running the forward pass: PyTorch records it dynamically on every iteration (define-by-run), while early TensorFlow built the whole graph statically before execution. Its central use is reverse-mode automatic differentiation: every node carries a known local derivative, so the chain rule becomes a mechanical backward sweep that produces every parameter’s gradient in a single pass, whose cost is roughly 2-3 times the forward pass and does not grow with the number of parameters being differentiated. The same data structure also drives memory planning such as activation checkpointing, whole-graph compiler optimizations like kernel fusion in XLA or torch.compile, device placement, and deployment export to formats such as ONNX.

(1) Structure and Evaluation: nodes are ops or values and edges are tensors; the forward pass evaluates nodes in topological order and caches every intermediate, because the backward sweep needs those values to compute local derivatives.
(2) One Sweep, All Gradients: each node multiplies its incoming adjoint \bar{v} = \partial L / \partial v by its local Jacobian and passes the result to its inputs, summing contributions at fan-out; a single reverse traversal yields gradients for all parameters, which is why backprop scales to billion-parameter models.
(3) Dynamic vs Static Graphs: PyTorch’s define-by-run graph follows arbitrary Python control flow and debugs like ordinary code; static graphs (TensorFlow 1, exported ONNX) trade that flexibility for ahead-of-time whole-graph optimization and portable deployment.

Forward computational graph of L = (wx + b - y)^2: input boxes x=2, w=3 feed a multiply node producing u=6, then an add node with b=1 producing z=7, a subtract node with y=5 producing e=2, and a square node producing L=4

Figure 1: Forward pass of L = (wx + b - y)^2 with x=2,\ w=3,\ b=1,\ y=5: each op consumes its input tensors, emits one value, and caches it for the backward sweep.

The backward sweep traverses the same graph in reverse. Starting from the seed \bar{L} = 1, each node applies the chain rule locally: the square node returns 2e = 4, the subtraction forwards +4 to z and -4 to y, the addition copies its adjoint to both u and b, and the multiplication swaps operands, delivering \bar{w} = 4 \cdot x = 8 and \bar{x} = 4 \cdot w = 12. No node needs global knowledge of the loss; correctness of the whole sweep follows from composing local derivatives edge by edge. The figure shows \bar{x} and \bar{y} to make the mechanics uniform, but a framework skips those branches when the input and target carry requires_grad=False, so only \bar{w} and \bar{b} are actually materialized. When a value feeds several nodes (fan-out), its adjoint is the sum of the contributions along each outgoing edge, which is exactly the multivariate chain rule.

Backward pass over the same graph with arrows reversed: adjoint labels on every edge showing e-bar=4, z-bar=4, y-bar=-4, u-bar=4, b-bar=4, w-bar=4 times 2=8, x-bar=4 times 3=12, seeded by L-bar=1

Figure 2: The same graph in reverse: each node multiplies the incoming adjoint by its local derivative, so one sweep accumulates \bar{w} = 8 and \bar{b} = 4 at the leaves.

The graph pays for itself well beyond gradients. Since it records exactly which intermediates feed the loss, frameworks can plan memory: reverse mode must retain cached activations, so training a depth-N network costs O(N) activation memory, and gradient checkpointing cuts this to O(\sqrt{N}) by storing only boundary activations and recomputing each segment during the backward pass, at the price of roughly one extra forward evaluation. A static snapshot of the same graph lets compilers fuse ops, pick kernel layouts, and place tensors across devices, and exporters freeze it into a portable artifact. The trade-off is flexibility: a define-by-run graph is rebuilt every iteration, so data-dependent if-statements and loops simply work, while a static graph must capture control flow symbolically, which is why TensorFlow 2 defaulted to eager execution and recovers graph benefits selectively through tf.function tracing.

Mathematical Formulation:
L = (w\,x + b - y)^2
\bar{v}_i = \sum_{j \,\in\, \mathrm{children}(i)} \bar{v}_j \, \frac{\partial v_j}{\partial v_i}

Where:

  • L is the scalar loss; w, x, b, y are the weight, input, bias, and target scalars of the running example.
  • v_i is the output value of node i, and \bar{v}_i = \partial L / \partial v_i is its adjoint, the gradient of the loss with respect to that node’s output.
  • \mathrm{children}(i) are the nodes that consume v_i; each child contributes its incoming adjoint times the local partial \partial v_j / \partial v_i, and the results sum, which is the multivariate chain rule.
AspectDynamic Graph (PyTorch Eager)Static or Compiled (TF1, ONNX, torch.compile)
Graph BuiltEvery forward pass, fresh per iterationOnce, ahead of execution or by tracing
Control FlowNative Python if/for; graph follows the dataSymbolic capture; data-dependent branches are hard
DebuggingStandard Python debugger on eager valuesGraph-level tooling; values not materialized eagerly
OptimizationOp-by-op dispatch, limited cross-op rewritesWhole-graph fusion, layout, device placement
DeploymentNeeds an explicit export stepGraph is already a portable artifact

Login to view more content


Log in to track your progress

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *