An Error Controller for a Trained Net, in JAX

· 5 min read

#integrators#adaptive-step#depth#jax#flax#nnx#implementation

Part 5 of 5Networks as Integrators
  1. 1Your Skip Connection Is Half of Newton
  2. 2Transformers With a Velocity Ledger
  3. 3A Network Built from Hamiltonian Steps
  4. 4Backprop Without the Memory
  5. 5Depth on Demandthis post's explainer
Explainer companionDepth on DemandWant the full intuition first? This is the runnable companion to the explainer.Read the explainer

The controller here has no parameters and never sees a gradient. It takes one step, takes two half-steps, compares them, and either commits or halves and retries. Everything the explainer claims about depth becoming a dial after training follows from that comparison, run on a network trained once at fixed depth and never touched again. What is below is the renderer, the accounting that keeps rejected probes on the books, and the figures, all quoted from the Kaggle run (bundle kgl_blog-adaptive-v2).

Train fixed, render free

Training is the previous post’s leapfrog net, sixteen kick-drift-kick steps of a learned potential as a lax.scan, trained end to end and never touched again. The controller is pure inference. Its unit of work is one leapfrog evaluation, and its whole intelligence is a comparison:

def render_adaptive(p, x, tol, h0=T_TOTAL / L_TRAIN, h_min=T_TOTAL / 4096):
    z = np_affine(x[None], p["enc"])
    q, pm = z[:, :DIM], z[:, DIM:]
    t, h, work, accepted = 0.0, h0, 0, 0
    while t < T_TOTAL - 1e-9:
        h = min(h, T_TOTAL - t)
        q1, p1 = np_leap(p, h, q, pm)                    # one h step
        qh, ph = np_leap(p, h / 2, q, pm)                # two h/2 steps
        q2, p2 = np_leap(p, h / 2, qh, ph)
        work += 3
        err = float(np.max(np.abs(np.concatenate([q1 - q2, p1 - p2], 1))))
        if err > tol and h > h_min:
            h = h / 2                                     # too sharp: refine, retry
            continue
        q, pm = q2, p2                                    # accept the finer render
        accepted += 2                                     # two committed h/2 steps
        t += h
        if err < tol / 4:
            h = min(h * 2, T_TOTAL / 4)                   # gentle: stride out
    lg = np_affine(np.concatenate([q, pm], 1), p["dec"])[0]
    return lg, work, accepted

Two counters, deliberately kept apart. work counts every leapfrog evaluation, including the probes of steps that were rejected, because that is what the controller actually costs; accepted counts only committed steps, the rendered resolution. The explainer’s efficiency claims use work; the power law lives in accepted. Conflating them flatters the method by a factor of about two, so the run keeps both.

A real input stepped through the trained flow by the controller: accepted states accumulate along the trajectory, a rejected probe flashes as an X, and the step-size bar below breathes, shrinking at stiff stretches and doubling on gentle ones
The controller on one real spirals input, from the exported weights. Blue dots are accepted states; the X is a rejected probe forcing a halve-and-retry; the bar is the step size breathing against the dashed training step. Nothing here is scheduled; each halving is the error estimate at that moment coming back too large.

The same decisions, seen as the step size breathing in time, once per tolerance:

Three staircase traces of step size against integration time for tolerances 0.3, 0.03, 0.003, each drawing forward in time, shrinking at the same stiff stretch of the flow and striding out on the gentle stretches
The step size along one input’s render at three tolerances, drawing in integration time. All three breathe at the same places, the stiff stretches of this input’s trajectory, and tightening the tolerance shrinks every step rather than adding uniform ones: 9, 15, and 36 accepted steps for the same journey. The dashed line is the uniform training step.

The dial in numbers

Sweeping the tolerance re-renders the same inputs at five precisions. The work histogram moves; the verdicts do not:

Five side-by-side histograms of per-input work at tolerances 0.3 to 0.003: the distribution slides right and spreads while the accuracy in each title stays fixed
The tolerance sweep on the trained spirals net, 100 held-out inputs re-rendered at each dial setting. The bill moves; the accuracy in the titles barely does. The distribution also spreads: precision is bought input by input, not in bulk.

The mean of that moving histogram is lawful. Accepted steps against tolerance on log-log axes, all three datasets, with the second-order method’s slope drawn once:

Log-log plot of accepted steps against tolerance for moons, rings, spirals, all three lying along a dashed slope minus one-third guide line
The measured accepted-step means across the tolerance sweep, three datasets, three seeds each. The dashed guide is the tol^(-1/3) a second-order integrator’s error control predicts; the measured log-log slope is 0.32.

Where the money goes

The run’s most useful negative result is the geography of effort. Rendering every point of the input plane at one tolerance and coloring by work:

The input plane being swept row by row, each cell colored by the work the controller spent there, with the data points overlaid; the bright expensive ridges do not follow the class boundary
The effort field computed row by row by the real renderer at tol 0.03, data points overlaid. The expensive ridges follow the stiff regions of the learned potential, not the decision boundary; the correlation between work and classification margin stays near zero on all seeds.

The explainer draws the moral; the implementation note is just that this field costs nothing to explore, because the controller is inference-only: no gradient, no retraining, one while-loop per pixel.

Every number above is from one run on Kaggle (bundle kgl_blog-adaptive-v2); the figures are rendered from the bundle’s exported seed-0 weights, replaying the identical controller arithmetic.

Cite as

Bouhsine, T. (). An Error Controller for a Trained Net, in JAX. Records of the !mmortal Data Scientist. https://tahabouhsine.com/blog/depth-on-demand-jax-flax-nnx/

BibTeX
@misc{bouhsine2026depthondemandjaxflaxnnx,
  author       = {Bouhsine, Taha},
  title        = {An Error Controller for a Trained Net, in JAX},
  year         = {2026},
  month        = {jul},
  howpublished = {\url{https://tahabouhsine.com/blog/depth-on-demand-jax-flax-nnx/}},
  note         = {Blog post, Records of the !mmortal Data Scientist}
}

References

  1. Hairer, E., Nørsett, S. P., Wanner, G. (1993). Solving Ordinary Differential Equations I: Nonstiff Problems. Springer.