Reported by @benedict-96 in review of #207 ("massive performance issues" with the SymbolicPullback). Measured; the summary is that the per-call performance is excellent and the build is what explodes, multiplicatively in n_integrators.
Per call, the symbolic pullback is what you want
dim = 4, nhidden = 1, batch 32, comparing SymbolicPullback(nn, ParametricLoss(), μ) with ZygotePullback(ParametricLoss()):
| network |
params |
ZygotePullback |
SymbolicPullback |
dim=2, width=2, n_int=1 |
32 |
10.4 ms |
0.1 ms |
dim=2, width=4, n_int=1 |
80 |
23.6 ms |
0.1 ms |
dim=4, width=4, n_int=1 |
88 |
33.6 ms |
0.1 ms |
dim=4, width=4, n_int=2 |
176 |
140.1 ms |
— (never builds) |
The symbolic call is flat at 0.1 ms while Zygote degrades from 10 ms to 140 ms — a 100–1000× advantage that grows with the network. So this is worth fixing rather than abandoning.
The build explodes with n_integrators
Phase-by-phase, dim = 4, width = 4, nhidden = 1:
| phase |
n_integrators = 1 |
n_integrators = 2 |
| symbolic variables |
0.08 s |
0.01 s |
| build the symbolic loss |
0.87 s, 3.4 ⋅ 10⁵ chars |
0.03 s, 1.4 ⋅ 10⁹ chars |
symbolic_differentials |
0.03 s |
0.01 s |
symbolic_derivative |
0.66 s, 1.5 ⋅ 10⁸ chars |
never finishes |
build_nn_function |
1.34 s |
— |
Two observations:
- The loss expression grows ~4,100× when
n_integrators goes 1 → 2, before any differentiation. At two integrators it is already 1.4 billion characters; the process passes 8 GB and does not return.
- Differentiating multiplies by a further ~440× (3.4 ⋅ 10⁵ → 1.5 ⋅ 10⁸ at one integrator).
Cause
Each SymplecticEuler layer's forward pass calls the executable gradient that build_gradient produced for its energy network. Traced on symbolic Nums, that call inlines the whole gradient expression. Stacking integrators inlines that expression inside itself, so the loss grows multiplicatively in the depth of the chain rather than additively. cse = true in build_nn_function cannot help: it runs at code generation, long after the expression has been materialised.
Suggested fix
Do not trace the inlined chain. Compose the pullback layer by layer — each SymplecticEuler has already built the gradient of its own energy network, so the chain rule can reuse those instead of re-expanding them. That keeps the expression linear in depth.
This is upstream-shaped work: it needs either a layerwise pullback protocol in SymbolicNeuralNetworks, or a build_nn_function that can take a composition without inlining it. Related: #243, which tracks the type piracy in the same area.
For now
SymbolicPullback(nn, ::ParametricLoss, μ) throws an ArgumentError for n_integrators > 1 rather than appearing to hang (GML #207). n_integrators = 1 builds in ≈1.4 s and is fully supported. The default training path uses ZygotePullback and is unaffected.
🤖 Generated with Claude Code
Reported by @benedict-96 in review of #207 ("massive performance issues" with the
SymbolicPullback). Measured; the summary is that the per-call performance is excellent and the build is what explodes, multiplicatively inn_integrators.Per call, the symbolic pullback is what you want
dim = 4,nhidden = 1, batch 32, comparingSymbolicPullback(nn, ParametricLoss(), μ)withZygotePullback(ParametricLoss()):ZygotePullbackSymbolicPullbackdim=2, width=2, n_int=1dim=2, width=4, n_int=1dim=4, width=4, n_int=1dim=4, width=4, n_int=2The symbolic call is flat at 0.1 ms while Zygote degrades from 10 ms to 140 ms — a 100–1000× advantage that grows with the network. So this is worth fixing rather than abandoning.
The build explodes with
n_integratorsPhase-by-phase,
dim = 4, width = 4, nhidden = 1:n_integrators = 1n_integrators = 2symbolic_differentialssymbolic_derivativebuild_nn_functionTwo observations:
n_integratorsgoes 1 → 2, before any differentiation. At two integrators it is already 1.4 billion characters; the process passes 8 GB and does not return.Cause
Each
SymplecticEulerlayer's forward pass calls the executable gradient thatbuild_gradientproduced for its energy network. Traced on symbolicNums, that call inlines the whole gradient expression. Stacking integrators inlines that expression inside itself, so the loss grows multiplicatively in the depth of the chain rather than additively.cse = trueinbuild_nn_functioncannot help: it runs at code generation, long after the expression has been materialised.Suggested fix
Do not trace the inlined chain. Compose the pullback layer by layer — each
SymplecticEulerhas already built the gradient of its own energy network, so the chain rule can reuse those instead of re-expanding them. That keeps the expression linear in depth.This is upstream-shaped work: it needs either a layerwise pullback protocol in
SymbolicNeuralNetworks, or abuild_nn_functionthat can take a composition without inlining it. Related: #243, which tracks the type piracy in the same area.For now
SymbolicPullback(nn, ::ParametricLoss, μ)throws anArgumentErrorforn_integrators > 1rather than appearing to hang (GML #207).n_integrators = 1builds in ≈1.4 s and is fully supported. The default training path usesZygotePullbackand is unaffected.🤖 Generated with Claude Code