Tensor Flow 📂 Variables and Automatic Gradients · 4 of 5 35 min read

Persistent GradientTape and Higher-Order Gradients

Learn why a normal GradientTape works only once and how persistent=True lets you call gradient many times. Then compute second derivatives with nested tapes and use them in Newton's method. Play with a tape lifecycle simulator and a derivative ladder that plots f, f′ and f″ for any function you type.

Section 01

The Story — A Single Ticket and a Season Pass

Watch Once, or Watch Many Times?
A normal cinema ticket lets you watch the film once. After that, the ticket is torn and thrown away. A season pass lets you come back as many times as you like — but you must hand it back at the end of the season.

A normal GradientTape is the single ticket. After one call to tape.gradient(), TensorFlow frees the recording. A persistent=True tape is the season pass. You can call gradient() many times, and you delete it when done.

And what about the "gradient of a gradient"? Think of a car. The gradient of position is speed. The gradient of speed is acceleration. That is a second-order gradient.

Section 02

A Normal Tape Works Only Once

To save memory, a tape throws away its recording right after the first gradient() call.

import tensorflow as tf

x = tf.Variable(3.0)
with tf.GradientTape() as tape:
    y = x ** 2
    z = x ** 3

print("dy/dx =", tape.gradient(y, x).numpy())
try:
    tape.gradient(z, x)                        # second call
except RuntimeError as e:
    print("RuntimeError:", str(e).split(".")[0])
OUTPUT
dy/dx = 6.0 RuntimeError: A non-persistent GradientTape can only be used to compute one set of gradients (or jacobians)

Section 03

persistent=True — Many Gradients From One Recording

x = tf.Variable(3.0)
with tf.GradientTape(persistent=True) as tape:
    y = x ** 2
    z = x ** 3

print("dy/dx =", tape.gradient(y, x).numpy())   # 2x  = 6
print("dz/dx =", tape.gradient(z, x).numpy())   # 3x² = 27
del tape                                        # free the memory yourself
OUTPUT
dy/dx = 6.0 dz/dx = 27.0
💾
Persistent Tapes Hold Memory

A persistent tape keeps every intermediate tensor alive until the tape is deleted. With a big model this can be many gigabytes. Always del tape when you are finished, and only use persistent when you really need more than one call.

💡
Many Variables Do Not Need a Persistent Tape

Gradients of one target for many variables come from a single call: tape.gradient(loss, [w, b]). You only need persistent for several targets (for example two different losses) or for jacobians.


Section 04

Try It — Tape Lifecycle Simulator

🎞️ Record, Replay, Release Interactive

Choose the tape type, press Record, then ask for gradients. Watch when the tape gets released. Here x = 3.0, y = x ** 2, z = x ** 3.


Section 05

Higher-Order Gradients — Nested Tapes

To get the gradient of a gradient, put one tape inside another. The inner tape computes the first gradient. Because that happens inside the outer tape's block, the outer tape records it — and can differentiate it again.

x = tf.Variable(2.0)

with tf.GradientTape() as outer:
    with tf.GradientTape() as inner:
        y = x ** 3                      # y   = x³   = 8
    dy_dx = inner.gradient(y, x)        # y'  = 3x²  = 12   (recorded by outer)
d2y_dx2 = outer.gradient(dy_dx, x)      # y'' = 6x   = 12

print("y   =", y.numpy())
print("y'  =", dy_dx.numpy())
print("y'' =", d2y_dx2.numpy())
OUTPUT
y = 8.0 y' = 12.0 y'' = 12.0
Animated Diagram — The Outer Tape Records the Inner Tape's Work
outer tape: records everything below inner tape x = 2 y = x³ = 8 y' = 3x² = 12 inner.gradient y'' = 6x = 12 outer.gradient

Blue: the inner tape finds the first derivative. Purple: the outer tape differentiates that result again.

A Persistent Tape Can Do It Too

x = tf.Variable(2.0)
with tf.GradientTape(persistent=True) as tape:
    y = x ** 3
    dy = tape.gradient(y, x)       # computed while still recording
d2y = tape.gradient(dy, x)
print(dy.numpy(), d2y.numpy())
del tape
OUTPUT
12.0 12.0

Section 06

Try It — The Derivative Ladder

🪜 f, f′ and f″ Together Interactive

Type a function of x or pick a preset. Move the slider. See the value, the slope (f′) and the curvature (f″). Where f′ = 0 and f″ > 0 you are at the bottom of a valley — a minimum.

f(x)f′(x) — slopef″(x) — curvature

Section 07

Where Second Derivatives Are Used

One classic use is Newton's method. Plain gradient descent only knows the slope. Newton's method also knows the curvature, so it can jump much closer to the minimum in one step: x ← x − f′(x) / f″(x).

x = tf.Variable(2.0)                         # minimise f(x) = x**4 - 3x

for step in range(5):
    with tf.GradientTape() as outer:
        with tf.GradientTape() as inner:
            f = x ** 4 - 3 * x
        g = inner.gradient(f, x)             # f'
    h = outer.gradient(g, x)                 # f''
    x.assign_sub(g / h)                      # Newton step
    print(f"step {step}: x = {x.numpy():.6f}   f'(x) = {g.numpy():+.4f}")

print("exact answer:", round((3 / 4) ** (1 / 3), 6))
OUTPUT
step 0: x = 1.395833 f'(x) = +29.0000 step 1: x = 1.058869 f'(x) = +7.8783 step 2: x = 0.928887 f'(x) = +1.7488 step 3: x = 0.909002 f'(x) = +0.2059 step 4: x = 0.908561 f'(x) = +0.0044 exact answer: 0.90856
UseWhat it needsTool
Two losses from one forward passSeveral gradient callspersistent=True
Newton's method, curvature checks2nd derivativeNested tapes
WGAN-GP gradient penaltyGradient of a gradient normNested tapes
Physics-informed neural networksdu/dx, d²u/dx² of the networkNested tapes
Per-output gradientsA full Jacobiantape.jacobian, tape.batch_jacobian

Section 08

Golden Rules

🎞️ Persistent and Higher-Order — Rules to Remember
1
A normal tape allows one gradient() call. A second call raises RuntimeError.
2
Use persistent=True for several targets, then del tape to free memory.
3
Many variables with one target never need a persistent tape. Pass a list of variables.
4
For a 2nd derivative, compute the first gradient inside the outer tape's block.
5
f′ = 0 with f″ > 0 means a minimum; with f″ < 0, a maximum.