Fitting & optimization — implementation internals#

Contributor-facing rationale behind the fitting stack. The user-facing reference — structure, usage, and the design properties users rely on — is ../fitting_and_optimization.md; this note carries the engineering decisions, tradeoffs, and deferred work.

Backend-first (the razor)#

The algorithms live in the backend (backend/optimizers.py), operating on raw cores / tangent tuples via backend functions only, free of the numerical safety preconditions (which live in the frontend). The frontend optimizers.py is a thin validate-once adapter. Why: an important minority of users bypass the OO frontend and work on raw .data tuples — they must be able to run the same optimizer code. It also means jit just works (no unsafe() wrapping — the backend has no checks to skip).

Low-memory transpose: adjoint-state over scatter (store-vs-recompute)#

The gradient 𝒥ᵀr for apply/entries needs a right context beyond the forward’s left sweep. Two ways: the scatter stores the full frame sweep (xi,mu,nu,eta) (cheap matvec, more memory), or the adjoint-state method recomputes the right context as a seeded sigma_hat reverse sweep (stores only (xi,mu), costs a sweep per matvec). T3Toolbox uses adjoint-stateexactly 2× less W-scaling memory. Why: at real scale the W-batched edge variables get large; memory is the binding constraint on a 40GB GPU, worst for minibatched Newton-CG (smaller batches → more ill-conditioned H → wants more data → larger W). It is the classic checkpointing tradeoff, and the project’s standing preference is low memory over compute (trade ~2× compute for ~2× memory on W-scaling ops). Probe can’t use it — its residual is a vector (one free mode), which must be propagated by the full adjoint sweeps + nu/eta; the scalar-seed shortcut is apply/entries-only. This is why probe’s precompute is full and apply/entries’ is lean.

numpy einsum: force BLAS-eligible pairwise paths; jax: one big einsum#

The grouped contractions (backend/contractions.py) route through _grouped_einsum. numpy: a forced greedy-pairwise path — because numpy’s optimize=True minimizes FLOP count, and on a FLOP-tie runs a single multi-operand contraction as one c_einsum loop with no BLAS (10–55× slower for the high- dimensional order-combines). jax: one big einsum — XLA’s opt_einsum + fusion is BLAS-aware and beats any path we force. Why it matters here: the derivative forward/transpose are dominated by those order-combines; the fix is 11–19× on them, numerically identical.

Design-rationale tails (history and naming)#

  • Geometry as a bundle was the geometry refactor’s core move: one geometry-generic GaussNewtonModel/Problem, so optimizers are written once, not per geometry (six classes collapsed to one model + two geometries).

  • The precompute / *_from_sweep split is deliberately named and public in the backend (rather than a private cache) so a raw-.data user composes public functions with docstrings — the razor’s “capability, not line count” corollary.

  • Order-slicing is not a draw: order is an output-only axis — the forward computes the whole jet jointly, so subsetting orders is output-masking, not input-slicing. Deferred, and most naturally an outer continuation loop anyway.

Regularization#

A regularizer is an objective term ρ(x) folded into the backend LocalModel (objective/gradient/gn_quadratic/hvp) and Problem.objective, so it reaches every optimizer and kind for free (they only ever touch those surfaces). It is backend-homed (backend/regularization.py, check-free) and re-exported, so a raw-.data user attaches it with the same one kwarg as the frontend. Why an objective term and not Newton-Hessian-only damping (decision D2): damping the Hessian alone is invisible to the objective and gradient, so the first-order optimizers (gradient_descent/mc_sgd/adam) and even Newton’s line search would be unregularized. To reach all of them, regularization must be a term in the objective, hence contribute to the gradient.

Identity reg is ρ = ½λ‖x‖² in the geometry’s own tangent metric (decision D1) → it contributes H_R = λ·project (= λp for a gauged tangent). On the manifold the tangent coordinate metric is the Hilbert–Schmidt metric, so this is exactly the ambient ridge ½λ‖x‖²_HS; on corewise it is weight decay ½λΣ‖core‖² (which, as a bonus, makes the otherwise gauge-singular corewise Newton Hessian strictly PD). Regularizing in each geometry’s own metric — rather than an ambient ridge bolted on some other way — is also what makes the uniform layer safe (below), because it inherits the mask-correctness of inner/project.

The base point as a tangent (v_X) — the non-obvious part. The reg gradient needs x in tangent coordinates. Key fact: the attachment point X is a single gauged tangent termall variations zero except the last TT variation, set to the frame’s last left core P_last (the one slot with no gauge condition, so it is already gauged; dense(v_X)=X). Hence point_tangent(frame)=v_X is a direct construction and point_norm_sq = ‖P_last‖²not the naive “sum of frame-core norms,” which is wrong (orthogonal frame cores have norm √rank, not content: measured 42291.8 vs the true ‖X‖²=42280.8). The tangent isometry (paper Appendix A.3) is stated for the zero-centered space but extends to X because v_X is itself gauged. A consequence worth knowing: on the manifold H_R = λI is the exact Riemannian Hessian (grad = x is fully tangent — scaling preserves rank — so the Weingarten curvature term vanishes), not just a Gauss–Newton approximation.

value(x) (the line-search objective) rides the retraction’s orthogonality. MANIFOLD.retractt3svd always emits left-orthogonal cores, so ‖x‖_HS = ‖x.tt_cores[-1]‖ — one core norm, no re-orthogonalization, and the left-orthogonalization is already paid to form the candidate for the data term. “Read the last core” is correct only for a left-orthogonal input — a numerical precondition, so per the house rule the backend value is check-free and the matching checker/orthogonalizer (t3_orthogonality_residual / t3_left_orthogonalize) are already public: a backend user self-guards with the same tools the frontend safe-mode uses. No new checker, no backend/frontend inequality.

Uniform mask-safety. ρ is a reduction, so a naive ½λ·Σ(supercore²) would sum the garbage padding in the out-of-mask rank slots — wrong and nondeterministic (the phantom-rank failure class, testing_strategy.md). Design rule: the regularizer routes only through mask-aware primitives (geom.inner/geom.project/the uniform tangent norms) and never indexes raw supercores, so it inherits their mask-correctness. g_R/H_R p are tangents (garbage inert) but their output masks must stay exact — project guarantees it. Tested with large-finite garbage padding (1e6·(1−mask)), not just dense-vs-ragged (which is blind to it).

Stochastic scaling. ρ is deterministic, but the minibatch data-gradient is a batch/n estimate of the full gradient, so mc_sgd/adam scale the reg by min(batch,n)/n per step (the generic _ScaledRegularizer wrapper), keeping the minibatch step ≈ the full step and λ’s meaning batch-invariant; the full-batch stop keeps full-strength reg. A custom draw of a non-nominal batch size may want λ retuned.

What’s deferred (not built)#

  • The Goal-1 fit(...) facade — a “just fit my tensor” entry point that picks a sensible geometry + optimizer, supplies the geometry-correct x0, and runs rank continuation with validation. The current layer is a clean mid-level toolkit; the facade is what delivers “standard user, no fiddling”. Rank continuation + validation currently live in the examples (the right defaults: manifold → zero start

    • warm continuation; corewise → nonzero start + cold per level — see dev/archive/optimizers_plan.md §7).

  • A Riemannian L-BFGS (with vector transport) — the quasi-Newton method only this library could provide; the Euclidean case is deliberately left to the scipy bridge.

  • Order-slicing minibatches (output-masking) and order/polynomial-degree continuation — research, likely outer loops.

  • The example pass — deciding which examples/fit_hilbert_* use the library optimizers vs keep inline to illustrate the hidden hooks (gn_hessian, gn_quadratic, corewise_map); dev/archive/optimizers_plan.md §10.

  • Per-sample gradients / multi-source fits (SVRG-style; fitting from applies and entries together) — reachable at the backend level (sum_over_probes=False; sum two local models), not packaged.

  • Grasedyck–Kramer regularization — a SingularValueRegularizer implementing the same four Regularizer methods with a frame-dependent SPD M built from the cores’ unfolding singular values (penalize poorly-determined, small-σ directions more). Drops in with no change to Problem/LocalModel/optimizers/kinds — the payoff of designing the protocol now — and is the sharper tool for denoising a good fit (where identity λ is only a modest secondary knob; user doc §5). Reuses point_tangent/project composed with an M-apply on tangents.

  • Base-point-as-tangent as a public op. Representing X as its gauged tangent v_X (last TT variation = P_last, else zero — already gauged) is broadly useful beyond the reg gradient; worth a first-class T3Tangent/UT3Tangent factory + backend helper. The reg’s point_tangent (backend/optimizers.py::_manifold_point_tangent; the uniform closure in uniform_fitting) is the current internal implementation to promote.

  • already_left_orthogonal retraction amortizationMANIFOLD.retract (t3svd) emits left-orthogonal cores, but the next step’s t3_orthogonal_representations re-orthogonalizes from scratch; already_left_orthogonal=True skips that sweep. Passing it after a retraction speeds every Newton step (needs the optimizer to carry a “came from retract” flag). Surfaced by the reg value(x) design, which relies on the same left-orthogonality.

Plans and research pointers#

  • dev/archive/optimizers_plan.md (the optimizers + example two-track plan), dev/archive/derivative_fitting_plan.md (the D1–D4 derivative-fitting build), dev/archive/geometry_refactor_plan.md (the geometry abstraction, incl. the corewise-vs-autodiff benchmarks behind the user doc’s §4.7), dev/archive/fitting_plan.md (the original fitting-layer build; its §9 records the exact-GN dense-truth oracle behind the user doc’s §4.2), dev/archive/safe_unsafe_mode_plan.md (the safe-mode preconditions the frontend enforces), dev/archive/regularization_design.md (the full regularization build — the D1/D2/D3 decisions, the v_X/value derivations verified this section summarizes, the uniform mask-safety spec, and the S4 Option-A regime exploration).

  • The polynomial/derivative fitting studies (MC-SGD minibatch findings, the symmetric-fit “halo”) live in a separate research repo (maintainer-local); the user doc’s §5 carries their practical takeaways.