Skip to content

SymbolicPullback build time grows multiplicatively with n_integrators #245

Description

@michakraus

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:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions