Chapter 16 — Key Takeaways (PennyLane)
The PennyLane page. §16.6's barren plateau is the fact that governs whether any of this scales.
The QNode
dev = qml.device("default.qubit", wires=1)
@qml.qnode(dev)
def circuit(theta):
qml.RY(theta, wires=0)
return qml.expval(qml.PauliZ(0))
theta = pnp.array(0.7, requires_grad=True)
circuit(theta) # 0.764842 = cos(0.7)
qml.grad(circuit)(theta) # -0.644218 = -sin(0.7), error 0.00e+00
No circuit object — the function body is the circuit. So parameters are plain Python arguments, and Chapter 8's ordering bug cannot occur here either (third distinct reason, after Cirq's dicts and Q#'s signatures).
The return declares the measurement: qml.expval · qml.probs · qml.sample · qml.state.
Expectation values are the default because they are what you differentiate.
pennylane.numpy wraps NumPy to track gradients. wires not qubits — PennyLane also supports
continuous-variable devices.
★ The parameter-shift rule
$$\frac{\partial f}{\partial\theta} = \frac{f(\theta + \pi/2) - f(\theta - \pi/2)}{2}$$
manual parameter shift = -0.644217688
qml.grad = -0.644217688
analytic = -0.644217688 error 0.00e+00
EXACT, not an approximation. For $U(\theta)=e^{-i\theta P/2}$ with $P^2=I$, $f(\theta)=A\cos\theta+B\sin\theta$ exactly, so two samples a quarter-period apart give the derivative.
vs finite differences
h estimate error
1e-01 -0.643144528 1.07e-03
1e-03 -0.644217580 1.07e-07
1e-05 -0.644217687 1.98e-11 <- sweet spot
1e-07 -0.644217688 2.91e-10
1e-09 -0.644217679 8.04e-09 <- worse again
Finite differences have a sweet spot and degrade on both sides (truncation error above, floating-point cancellation below). The parameter-shift rule has no step size to tune — $\pi/2$ is a large shift, which is why it survives on noisy hardware.
⚠️ Does not apply to every gate. A generator with >2 distinct eigenvalues needs more shift points.
The cost: exactly $2n+1$
parameters executions 2n+1
1 3 3
4 9 9
16 33 33
32 65 65
$$\text{shots} = (2n+1) \times \text{iterations} \times \text{shots per circuit}$$
100 params × 200 iterations × 4096 shots = 1.6 × 10⁸ shots. Charged every iteration — Chapter 12 §12.5's session argument.
⚙️ PennyLane skips parameters that cannot matter. 64 params across 4 wires, observable
PauliZ(0)→ 33 executions, 16 nonzero gradients (not 129). Also an ansatz bug detector: unexpected gradient zeros mean parameters disconnected from your observable.
★★★ Barren plateaus
qubits params Var[grad] max|grad|
2 36 1.0431e-01 0.8841
4 72 2.7964e-02 0.6131
6 108 8.0048e-03 0.3712
8 144 1.9115e-03 0.1664
10 180 4.6517e-04 0.0920
fit: Var ~ exp(-0.676 n) -> x0.5086 per qubit
halving every 1.03 qubits · Var(2)/Var(10) = 224x
×0.51 per qubit — within 2% of exactly one half. The textbook $\mathcal{O}(2^{-n})$ result, measured.
Why it is fatal, not annoying
Resolving a gradient of size $g$ needs $N \sim 1/g^2$ shots:
qubits Var[grad] |grad| shots to resolve
10 4.856e-04 2.204e-02 2.06e+03
30 6.523e-10 2.554e-05 1.53e+09
50 8.763e-16 2.960e-08 1.14e+15
~10¹⁵ shots per parameter per iteration at 50 qubits. The optimizer is not slow — it is blind. And 50 qubits is small: the plateau arrives long before the interesting problems do.
Shallower ansätze do not escape it
layers Var(4 wires) Var(8 wires) ratio
1 1.3842e-01 9.4414e-03 14.7x
2 4.3994e-02 2.1485e-03 20.5x
6 2.8436e-02 1.8937e-03 15.0x
You buy a constant factor in overall scale. The decay with width does not move.
🔬 What doesn't work: shallower ansätze (measured) · more shots (10¹⁵ is not a budget) · a better classical optimizer (the problem is absent information). What is proposed: problem-informed ansätze · smart initialization · layerwise training · local observables (real theoretical backing for shallow circuits — why §16.6 measures $Z_0Z_1$). None is a general solution. That is the honest state of the field.
★ Structurally zero ≠ barren plateau (CS1)
A first attempt sampled one parameter and got Var ~ 1e-33, |grad| ~ 1e-17 with a clean
exponential fit — an artifact 18 orders of magnitude more severe than the real effect.
StronglyEntanglingLayers opens with Rot = $R_Z R_Y R_Z$, so the first $R_Z$ acts on $|0\rangle$:
$$R_Z(\phi)|0\rangle = e^{-i\phi/2}|0\rangle \quad\Rightarrow\quad \partial\langle O\rangle/\partial\phi \equiv 0$$
g[0,0,0] first RZ on |0> = 9.714e-17 <- structurally zero
g[0,:,0] same angle, all wires = [0. 0. 0.]
g[2,1,1] a middle-layer angle = -0.143103 <- a real gradient
| Cause | Signature | Response |
|---|---|---|
| Structural | exactly ~1e-17 at every value and size | fix ansatz / exclude and log |
| Disconnected | exactly 0; gates on unmeasured wires | ansatz bug |
| Plateau | small but nonzero, varies per draw, shrinks with width | the real problem |
Discriminator: resample. A structural zero stays at 1e-17; a plateau gradient is a random variable that happens to be small.
A value near machine epsilon is an artifact until proven otherwise. And a convincing fit is not evidence of a correct measurement — the artifact was more internally consistent than the real signal, because floating-point noise is smoother than physics.
★ Diagnose the optimizer before the ansatz (CS2)
A 4-param ansatz stalled at −1.2485 (error 0.166) under GradientDescent(0.25), 40 steps.
ansatz energy error
shallow RY (4 params) -1.414214 6.66e-16 <- FINE
RY+RZ (8 params) -1.414212 1.33e-06
StronglyEntanglingLayers x3 (18) -1.414213 6.32e-08
The 4-parameter ansatz reaches machine precision — and is more accurate than either larger one.
GradientDescent(0.25), 200 steps -1.414214 2.28e-08 <- converges!
GradientDescent(0.05), 200 steps -1.259566 1.55e-01 <- WORSE
Adam(0.1) -1.414214 8.76e-09
Momentum(0.1) -1.414214 7.80e-10
It was simply too few steps — and reducing the step size, the instinctive fix, makes it worse.
Two diagnostics: 1. Is the trace still descending at the final step? If yes you ran out of steps — a stopping-criterion bug no extra parameters can fix. 2. Change the optimizer before the ansatz. If a better optimizer closes the gap, capacity was never the constraint.
Adding parameters to fix an optimizer problem moves every axis the wrong way: 4× the gradient bill (§16.4), a flatter landscape (§16.6), and a slightly worse answer.
Where PennyLane fits
| Task | PennyLane? |
|---|---|
| VQE, QAOA, quantum ML, gradients on hardware | yes — built for it |
| PyTorch / JAX / TensorFlow integration | yes |
| One codebase across backends | yes — pennylane-qiskit, pennylane-cirq |
| Low-level circuit control · transpilation | no — Qiskit |
| Explicit timing | no — Cirq |
| Resource estimation | no — Q# |
Often best used as a differentiation layer on top of Qiskit or Cirq rather than as a replacement.
Common pitfalls
- Sampling one parameter's gradient instead of all of them.
- Treating a ~1e-17 value as a measurement.
- Diagnosing a stall as ansatz capacity without changing the optimizer.
- Reducing the step size in response to a stall.
- Forgetting the gradient bill is $2n+1$ per iteration.
- Expecting a shallower ansatz to escape the plateau.
- Running without an independently-computed reference value.
Project piece added this chapter
vqelab/variational.py — gradient_cost() and measure_gradient_cost() ($2n+1$, verified against
the device tracker), classify_zero_gradient() (the three-way discriminator),
measure_gradient_variance() (reports exclusions rather than hiding them), and optimize()
returning a VariationalResult whose converged flag is false when the trace is still descending.
11 tests pass, including test_a_truncated_run_is_reported_as_NOT_converged and
test_gradient_variance_decays_exponentially_with_width.