tenet.ad¶
Stabilized JAX differentiation rules for svd and eigh at a degenerate spectrum.
tenet.ad ¶
Opt-in stabilized VJPs for svd/eigh on the JAX backend. Call install.
JAX's own SVD/eigh VJPs carry 1/(sigma_i - sigma_j) / 1/(w_i - w_j) factors that
are NaN at exact degeneracy, and under a non-Abelian symmetry degeneracy inside a
coupled sector is generic rather than an edge case. install() replaces them with the
Lorentzian-broadened form, 1/x -> x/(x**2 + eps); uninstall() restores autoray's
stock bindings.
Three things a caller must know:
-
It is process-global for the JAX backend, by design. The seam is
autoray.register_function("jax", "linalg.svd", ...), autoray's own extension point, so afterinstall()anyar.do("linalg.svd", jax_array)in the process -- quimb's included -- gets the broadened VJP. Installation is therefore an explicit function call, not an import side effect like tenet.pytree's: mutating another library's dispatch table is the user's act, not an import's. -
The broadened gradient is correct, not merely finite, exactly when the objective is gauge-invariant on each degenerate subspace. At exact degeneracy the singular vectors within a multiplet are only defined up to a unitary, so
dU/dAgenuinely does not exist; what exists is the derivative of gauge-invariant combinations such asU S Vh, the singular values, or any projector onto the multiplet. Broadening returns the correct value for those and aneps-suppressed arbitrary value for the rest. It is a precondition, not a hidden approximation: a gauge-dependent objective gets aneps-dependent answer, which is to say a meaningless one. -
epsis in units of sigma squared --safe_inverse(x) = x/(x**2 + eps)withx = sigma_j - sigma_i-- so a default tuned for anO(1)-normalized spectrum assumes a normalization our tensors do not guarantee. Hence the knob.
Broadening rather than a hard tolerance, because a hard cut makes the gradient
discontinuous in the spectrum. qr, lq and polar need nothing of their own:
their backend rules are already well defined where their inputs are full-rank.
tenet.enable_jax(ad=True) is the one-call spelling of the three statements
below, and is what the docs teach. It is the same explicit act this docstring demands --
ad is opted into by name there, never defaulted on, precisely because of the
process-global reach above -- and it runs this module's install() unchanged.
Usage::
import tenet
tenet.enable_jax(ad=True) # pytree registration + the broadened VJPs
def objective(t):
u, s, vh = tenet.linalg.svd(t) # per coupled sector
return tenet.norm(u @ s @ vh)
g = jax.grad(objective)(t)
install ¶
Swap JAX's stock linalg.svd/linalg.eigh VJPs for broadened ones.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
epsilon
|
float
|
The Lorentzian broadening, in units of sigma squared; the default is
the PRX value and assumes an |
1e-12
|
Notes
Process-global for the JAX backend, by design -- see the module docstring. Idempotent.
uninstall ¶
Restore autoray's stock JAX bindings. Idempotent.
register_function writes _FUNCS[backend, name]; deleting that entry is what
restores the stock binding, because the next lookup falls back through autoray's
import machinery and reapplies autoray's own wrapper for the function. Registering a
remembered "old" callable would instead leave a second override in place. Private
attributes, knowingly: autoray ships an install seam and no matching uninstall one,
and the only real caller is the test that needs stock and broadened gradients in the
same process.