Repository navigation
fix(coupling): an IQN step returns when its iterate leaves float range; the gradient bound leaves out a constant the pass does not resolve - #257
Merged
NicholasEhsanRoy merged 16 commits intoOct 7, 2026
Conversation
[skip ci]
…nto fix/p4-41-iqn-nonfinite-and-gradient-bound # Conflicts: # CHANGELOG.md
…e; the gradient bound covers the constants a pass resolves The secant least-squares of iqn-ils / iqn-imvj handed LAPACK's SVD a matrix holding an inf on the pass where part of the iterate overflowed, and the SVD does not return on such a matrix: step() never returned (MADD-ANO-231; v0.3.0 and v0.3.1 under solver=ift with iqn-imvj). The matrix is now replaced by zeros before the factorisation wherever the sum of its squares is not finite, and the quasi-Newton step is not taken; the same guard stands in front of every SVD of the coupling analysis. gradient_relative_error_bound took its worst over every probe with a non-zero tangent, including a constant whose tangent at the returned iterate is rounding (1.09 for a relative error of 1.134, usable; MADD-ANO-232). A probe is now in the bound where moving its constant by its own magnitude moves one pass by more than the residual's float floor. The step-program digests are removed here and recaptured on this commit: the programs of the eight gate graphs with an iqn-ils group change (a guard is new code); the other sixteen are compared with the earlier capture in the commit that adds the file. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
Captured on 457f4b4 under jax 0.10.2, 0.11.0 and 0.11.2, each in a fresh process. Against the capture it replaces (396eb59): sixteen of the twenty-four gate graphs hold the same digests in all three versions; the eight that moved are the choice-1 graphs of the named and sparse-ragged topologies, each of which has a group under acceleration=iqn-ils, whose secant least-squares now passes through the guard. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
… with diagnostics; Jacobi stalls finite) [skip ci]
…nto fix/p4-41-iqn-nonfinite-and-gradient-bound No content change: the branch already carried #255's head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
NicholasEhsanRoy
marked this pull request as ready for review
October 7, 2026 06:45
…base reached 232) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
…ependent fraction Whether a pass resolves a nonlinearity's own constant depends on where the float32 iterate stops: one block scores 0.17 to 0.57 of its examples on the development machine and none on CI's runners. The per-push seed holds that the score is not empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
NicholasEhsanRoy
deleted the
fix/p4-41-iqn-nonfinite-and-gradient-bound
branch
October 13, 2026 14:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two findings of the nonlinear coupling search (PR #255), fixed. Branch starts from that PR's branch and carries it (tests only);
origin/release/0.4.0at7acbe0d6(which has #255) is merged in.1.
step()did not return under IQN once the iterate left float range (MADD-ANO-231)Cause (measured, every probe under a wall-clock limit).
iqn_ils_updatesolves the secant least-squares withjnp.linalg.pinv, an SVD with vectors. On the pass where part of the iterate overflows, the new secant column holdsinfin those entries and finite numbers in the rest, and LAPACK's SVD does not return on such a matrix. On the pinned hub the eleventh call was handed an 8 x 119 matrix with fourinfentries, no NaN, and finite entries up to 5.8e285 (the ten before it were finite); the pass after would have added NaNs, and a matrix with a NaN beside theinfreturns.jnp.linalg.svd/pinvalone, float32 and float64, jaxlib 0.11.0, CPU: no return in 6 s (0.2 s where it returns) on 4 x 4, 12 x 8 and 16 x 16 matrices with oneinf, one-infor aninfcolumn, and on some with a row and a column atfinfo.max; an all-infmatrix, a NaN, and aninfwith a NaN return. It is not a loop condition: thewhile_loopcounter advances andnone/aitkenrun to the cap.Fix.
acceleration._lapack_input(A)returns(A or zeros, ok),okbeing that the sum of the squares is finite (so NaN,infand entries whose squares overflow are all withheld); the matrix is replaced before the factorisation (underjitboth sides of awhererun). Iniqn_ils_updatea withheld matrix means the quasi-Newton step is not taken (is_validcarriesok), so the Aitken fallback's iterate is returned, which is what a non-finitecalready meant. The group then runs to its cap and reportsconverged=False,residual=inf, exactly asnoneandaitkendo (the search's unaccelerated control readsRETURNED 120 False; so does IQN now). I did not add an early exit on a non-finite iterate: the cap is what CPL-053 / CPL-084 and the other accelerations do, and an exit in the loop condition would change the program of every looped graph.Sibling sweep: every LAPACK-backed call in
core/coupling/. 288 non-finite cases (8 patterns x 3 shapes x 8 primitives) and 576 extreme-finite cases (finfo.maxdown to subnormals) per dtype, each with its own time limit:acceleration.iqn_ils_updatepinv(SVD with vectors)inf_lapack_input); QN step not takenacceleration._spectral_analysissvd(I - Hk, compute_uv=False)diagnostics=Truefinfo.maxinfas before_bounds._full_resolvent_norm_and_rowsnorm(inv(I - M), ord=2),norm(T, ord=2)(SVD)I - Msingular or the basis non-finite_spectral_norm: NaN for a withheld matrix (what the SVD returned where it returned)_bounds._gradient_error_bound_bodynorm(dG, ord=2)batched,norm(resolve(change), ord=2)_spectral_normacceleration._spectral_radiuseigvalsacceleration._rounding_keeps_the_radiuseigvals,invfinite); unchangedacceleration.jacobian_range_basis,_bounds(two sites)qracceleration.resolvent_apply,_ift._dense,_boundssolve(LU)mapping.py(two sites)np.linalg.solveat build time, NumPyLinAlgErrorA NaN
any_dir(a non-finite derivative) still compares False inany_dir > 0and its term is dropped, as on the base; I have no reproducer of a wrong number from it and did not change it.Released? Yes. v0.3.0 and v0.3.1 (
git archiveof each tag, emptyHOME, 25 s limit):solver="ift"withacceleration="iqn-imvj"(jnp.linalg.lstsqthere) does not return on the test'sexppair in float32 or float64; the defaultsolver="fori"solved the normal equations by LU and returns a NaN state;iqn-ilsunderiftreturns. v0.2.1 and earlier have nosolver="ift".affected_versions: ">=0.3.0, <0.4.0".Values of finite runs. Bit-identical against the base (
75748b62), jaxlib 0.11.0: 200 random finite calls of the compilediqn_ils_update(five sizes, both dtypes, magnitudes 1e-100 to 1e100 in float64, rank-deficient, graded and empty column sets, with and without a previous pass; all eight outputs), and 108 drawn three-member nonlinear rings (both IQN kinds, both dtypes,iftGauss-Seidel and Jacobi,fori), two steps each: states, iterations, residual and every numeric key ofcoupling_diagnostics()exceptgradient_relative_error_boundon two (finding 2).Step-program digests. 8 of the 24 gate graphs moved, on all three jax versions, 5 programs each:
named/{chain-into-ring, nested-loop-additive, star-and-ungrouped-ring, two-groups-one-scc}/choice-1and the same four undersparse-ragged/. Each has a group underacceleration="iqn-ils"(topology_knobs(topo, 1)); no other gate graph has a quasi-Newton group, and none hasdiagnostics=True. The other 16 hold the same digests as the earlier capture (396eb59a) under 0.10.2, 0.11.0 and 0.11.2. Recaptured the waytest_step_program_digests_are_frozen.pyrequires: the fix commit457f4b4fremoves the file, the capture is taken on it in a fresh process per version (--write), andc3f10735adds it.scripts/compile_counts.py --checkon jax 0.10.2: counts match.2.
gradient_relative_error_boundbelow the error for a constant the fixed point does not respond to (MADD-ANO-232)Which quantity the bound covered on the two pins (float64 reference at the returned iterate):
r1.s0:g_k6.0e-26,g*2.2e-25, error 1.6e-25l1.s1:g_k1.5e-34,g*2.0e-35, error 1.7e-34So the absolute error was always tiny beside the gradients that have a size; what failed is a relative statement about a gradient that has none. On the ring the float32 tangents of the centres and curves are exactly zero and were never in the bound (the search scored the float64 tangent at the float32 iterate). On the hub the right-hand sides of those probes were 2e-33 to 5e-18 beside ones of order one: for the saturating
c + tanh(s (u - c)) / s,d/dsis two terms of sized / sthat cancel to-(2/3) s d^3, 24 orders below either atd = 1e-12, so the tangent is what the cancellation left.Choice: (b), per constant instead of per group. A statement relative to the whole gradient vector (a) would mix the units of different constants and silently weaken the per-constant claim the earlier rounds verified. Withdrawing the flag where the bound exceeds about 1 would withdraw it wholesale on these cells: wherever such a constant is resolved (an iterate stopped short) its relative error really is of order one, the bound reads 1.03 to 16 and holds, and that is information. So: a probe is in the worst only where the pass resolves its constant: moving the constant by the probe's whole size (its own magnitude) moves one pass from
x_kby more than the residual's float floor (the floorspectral_error_boundadds, in the bound's norm). This is the existing zero-tangent rule ("the fixed point does not respond to the probe") made at the resolution it can be made at.gradient_bound_usableis unchanged unless no probe is left (NaN, as before for "responds to no constant"). The docstring (coupling_diagnostics), the algorithm guide, CPL-093's conditions and the search's score (PassReference.pass_responsesaboveRESOLVED_MARGIN = 2floors) say the same thing.What it costs, said plainly. The rule is a magnitude, so a gain or forcing so weak that its whole value moves the pass by less than the floor is left out too, although its tangent may be exact. I looked for a measured quantity that tells the two apart and found none: the tangent's sensitivity to a floor-sized change of the state (the bound's own undirected term) read 1.6% for the hub's curve, whose relative error is 113%, because the cancellation is inside the node's derivative, not in the state. On the 108 drawn rings above the bound moved on two (2.27e-5 to 6.8e-6; 1.8e-2 to 2.5e-4: a weak gain was the worst probe) and no flag changed. Registered as the entry's residual risk; a constant left out is not named in the report. For the orchestrator: if weak-but-exact constants must stay covered, the alternative is one more JVP per probe to test whether the right-hand side depends on the iterate at all (additive constants could then be kept at any size); I did not add cost to the diagnostics without being asked.
Usable fraction, same draws before and after (the search's strategy, derandomised; 142 distinct draws on each of two blocks of seven cells; base
75748b62against this branch, jaxlib 0.11.0):gradient_bound_usable, beforeNo flag is withdrawn. The bound stops reading "of order one" on 39 and 38 draws where every constant with a size has a small error, and still reads one or more wherever a nonlinearity's constant is resolved. In the slow hunt the worst scores (limit 1) are 0.036 to 0.989 over gains, biases and weights and 0.656 to 0.99999631 over the nonlinearity constants, by block; the fraction of examples with such a constant scored is 0.17 to 0.57; the usable fraction 0.86 to 1.00. The linear search's gradient hunt (all seven blocks, not one seed): nothing over the limit, worst 0.942 to 0.999, usable 0.92 to 1.00.
Why the earlier every-constant sweep did not see this.
test_differential_gradient_bound.pyand the far-from-the-fixed-point tests measure the bound over every gain and bias of linear groups and overa,gof maps such asx <- a + g u**2. Every one of those constants enters the pass at first order with a sensitivity of order one at the fixed point: a gain multiplies an input that is not zero, a bias is added. Sog*is never zero,g_kis never small beside it, and a relative error is well defined at every iterate. The search's cells centre each nonlinearity on its input at the fixed point (phi(c) = c,phi'(c) = 1), which makes the pass's sensitivity tocandsvanish exactly there: the first constants in any harness whose true gradient is zero while the computed one is not.Tests
tests/core/test_coupling_iqn_returns_on_a_non_finite_iterate.py(new). Per push: the step returns within 30 s (it takes under 2) for both IQN kinds in float32 and float64,converged=False,residual=inf, 40 of 40 passes, on a pair whose first entry isexpof itself beside three that contract (fails on the base: no return);iqn_ils_updateon eleven kinds of poisoned input at three sizes in both dtypes, with the Aitken fallback's iterate returned for a withheld matrix;_lapack_input/_spectral_normoninf,-inf, NaN andfinfo.max. Slow: the step underfori, with diagnostics, underl2andinterface, Jacobi, withaitkenandnonebeside. A stuck call runs in a daemon thread and fails the test; the process then leaves byos._exit(1)at exit (a thread stuck in LAPACK aborts the interpreter's finalisation otherwise).tests/property/test_coupling_nonlinear_search.py: the two IQN pins un-xfailed and held toRETURNED 120 False; the skip of examples that leave float range on an IQN cell removed (leaves_float_rangeis gone, every example is stepped);gradient_vanishingis now one of the searches (per push and in the hunt, where the fraction of examples with such a constant scored must be above zero: measured 0.17 to 0.57 by block); a per-push seed with six of twelve centres and curves resolved (bound 1.27, error 0.79 of it); the two gradient pins are regression tests (test_a_constant_the_pass_does_not_resolve_is_not_in_the_gradient_bound: none of the nonlinearity's constants is resolved, the bound is below 1e-5 and not below the gains' error).tests/property/coupling_reference.py:PassReference.pass_responses.Mutants (scratch copy of
src/,plans/tools/mutants.py): 9 of 9 caught._lapack_inputreturns the matrix unreplacedtest_no_svd_...okis "every entry finite" (squares may overflow)test_no_svd_...(finfo.max)test_the_secant_update_returns_...test_no_svd_...rhs >= 0)test_coupling_gradient_bound_poisoned_by_a_non_finite_tangent.pyNo mutant for the amplification's guard: zeros and a NaN singular value both read
infthere, so removing the select is equivalent (I removed it).Code paths newly enabled or changed
diagnostics=Truethe analysis runs at a non-finite state (tested per push without and slow with diagnostics,foriandift, three norms, both schedules).iqn-imvjcan now carry non-finite columns into the next step of a graph that returned a non-finite state; that step's matrix is withheld the same way.gradient_relative_error_boundcan be smaller than before on a group with a weak or a non-responding constant, and is NaN (unusable) where no probe is resolved.Records
MADD-ANO-231 (shipped,
>=0.3.0, <0.4.0, major, resolved) and MADD-ANO-232 (never shipped, major, resolved); two CHANGELOG lines under Fixed; one line each in the release notes' Resolved paragraph; SOUP table regenerated; CPL-053, CPL-084 (tests added) and CPL-093 (conditions and tests);testing_standards.md(the search's skip is gone, the second gradient score).Run locally (three cores, jaxlib 0.11.0 unless said)
Every slow test of every file that cites
gradient_relative_error_bound/gradient_bound_usable,iqn_ils_update, the resolvent and spectral helpers, the two search modules and the digest gate, once each on the final code:tests/core(the 24 that cite the gradient bound or its helpers,test_coupling_acceleration.py,test_coupling_accelerators_are_units_invariant.py, and the new file),-m slow: 105 passed, 6 failed, all six in the new file's own slow test and all its premises, not the library (forireports only withdiagnostics=True; under Jacobi the quasi-Newton iterate of the fixture stalls finite, on the base too). Corrected and the new file rerun whole: 28 passed (8 per push, 20 slow).tests/property:test_coupling_nonlinear_search.py,test_coupling_targeted_search.py,test_differential_coupling_interactions.py,test_differential_fixed_point.py,test_differential_gradient_bound.py,test_differential_precision.py,test_differential_transforms.py,test_differential_coupling_solvers.py,test_interface_reading_twins.py,-m slow: 340 passed (one hour).tests/compliance/test_step_program_digests_are_frozen.pyandtests/core/test_step_program_digests.py,-m slow: 22 passed.tests/core(80 passed), the registry / claims / release-notes / guard-mutation / digest-frozen / slow-rule / sharding compliance tests, the three source scans, everyscripts/check_*.py, pyright 1.1.414 on the two changed modules (0 errors),compile_counts.py --checkon jax 0.10.2."iqn-ils"/"iqn-imvj"(the bit-identity runs above stand for them), and nothing on jax 0.10.2 or 0.11.2 except the digest captures and the compile counts.One thing to watch in the slow lane on other jax versions: the nonlinearity-constant score is tight by construction (the true relative error is 1 and the bound is it times
distance / ||delta|| >= 1): the worst here is 0.99999631 of the limit after the score's own allowances.🤖 Generated with Claude Code
https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD