Reduce float32 fields through muGrid, and floor the inner-CG tolerance once - #16
Merged
Merged
Conversation
Two float32 defects that only show up at scale, and one that had drifted. muTopOpt reduced raw field buffers with xp.dot / xp.sum. On a float32 field that is BLAS sdot: a float32 accumulator, whose error grows *linearly* in the number of entries. Measured against a double reference: 4.2e-9 at 32^3, 4.1e-7 at 64^3, 5.5e-6 at 128^3, ~4.6e-4 at 512^3. Nothing else in the solve degrades with resolution like that -- for contrast, the single- precision FFT round-trip grows only 37% from 32^3 to 512^3. muGrid's linalg.norm_sq / vecdot / axpy_norm_sq accumulate and return in double and stay flat at ~1e-14. They also copy nothing. Field.p is a strided view whenever the collection carries ghosts, and both engines always build with ghosts=1, so every .p.ravel() silently materialised a full contiguous copy of the field -- dim*N*4 bytes per call, on the device for GPU runs. And b_norm is now bit-identical to the ||b|| the CG converges against, instead of a float32 denominator under a double-accumulated residual. The consistent-objective correction -lambda^T r gets the same treatment. It is both the reported objective and the input to the trust region's accuracy control, so it carries the tightest error budget here. Separately: float32 eps is 1.19e-7, so the true residual b - Kx stagnates around 1e-6 while only the recursive CG residual keeps shrinking. Asking for less burns cg_maxiter and is rescued by the stagnation guard with a worse iterate than the reachable tolerance would have given. That floor existed in simulate.py alone, which is exactly why it had leaked: it was skipped by the "adaptation disabled" branch (which reassigned cg_tol_min *after* the clamp), by an explicit --cg-tol-min (warned but not applied), by simulate_conduction.py entirely, and by the library defaults cg_tol=1e-8 and cg_tol_min=1e-10. It is now applied wherever a tolerance is consumed, so no branch can route around it. AdaptiveInnerTolerance is deliberately left alone -- it stays a pure dtype-agnostic controller, and its tests pin that. cg_tol defaults to None and resolves per precision. An unreachable tolerance the caller asked for warns; a default they never chose is adjusted quietly. Needs muGrid 1.4.0: without the reduction fix norm_sq would return inf on exactly the large float32 solves this is for, and FLOAT32_RTOL_FLOOR (the shared constant muTopOpt.precision reuses) does not exist before it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 float32 defects that only show up at scale, and one that had drifted. Found while chasing non-convergence of 512³ single-precision runs.
Requires muGrid 1.4.0 (muSpectre/muGrid#208) — the pin in
pyproject.tomlis not cosmetic, see the last section.1. The reductions bypassed muGrid
solve_rhsand the consistent-objective correction reduced raw field buffers withxp.dot/xp.sum. On a float32 field that is BLASsdot: a float32 accumulator, whose error grows linearly in the number of entries. Measured against a double reference:xp.dotlinalg.norm_sqNothing else in the solve degrades with resolution like that. For contrast, the single-precision FFT round-trip grows only 37% from 32³ to 512³ (1.39e-07 → 1.91e-07), so the FFT is not what changes.
They also copied.
Field.pis a strided view whenever the collection carries ghosts, and both engines always build withghosts=1, so every.p.ravel()silently materialised a full contiguous copy —dim*N*4bytes per call, on the device for GPU runs.And
b_normis now bit-identical to the||b||the CG itself converges against, instead of a float32 denominator under a double-accumulated residual.The consistent-objective correction
-λᵀrgets the same treatment. It is both the reported objective and the input to the trust region's accuracy control, so it carries the tightest error budget here.2. The tolerance floor had leaked
float32 eps is 1.19e-7, so the true residual
b - Kxstagnates around 1e-6 while only the recursive CG residual keeps shrinking. Asking for less burnscg_maxiterand is rescued by the stagnation guard with a worse iterate than the reachable tolerance would have given.That floor lived in
simulate.pyalone — which is exactly why it had drifted. Audit of every path that setcg_tol_min:simulate.pydefaults (TR and L-BFGS)simulate.pyexplicit--cg-tol-minsimulate.py--cg-tol-start <= 0+ TRcg_tol_minafter the clampsimulate.pyHomogenization(cg_tol=args.cg_tol)simulate_conduction.pyoptimize.py_make_inner_tolerancecg_tol=1e-8It is now applied wherever a tolerance is consumed, so no branch can route around it.
AdaptiveInnerToleranceis deliberately left alone — it stays a pure dtype-agnostic controller, and its three existing tests pin that.cg_toldefaults toNoneand resolves per precision. A tolerance the caller explicitly asked for and cannot have warns; a default they never chose is adjusted quietly.Behaviour change worth a look: an explicit unreachable
cg_tolis now raised to the floor, not just warned about. I think that is right — the alternative is burningcg_maxiterand salvaging — but it is a behaviour change, not only a diagnostic.3.
simulate_conduction.pycaught upIt never had the precision-aware block
simulate.pyhas: nortol_floor, and a hard-codedbfgs_gtol = 2.5wheresimulate.pyuses25.0in single precision.args.precisionwas used only for the dtype, the banner and a NetCDF attribute.Why the muGrid pin is hard
Without muGrid 1.4.0's reduction fix,
linalg.norm_sqreturnsinfon exactly the large float32 solves this PR routes through it — worse than what it replaces. AndmuGrid.Solvers.FLOAT32_RTOL_FLOOR, the shared constantmuTopOpt.precisionreuses so the two packages cannot disagree about the floor, does not exist before it.Testing
172/172 pass, serial and under
mpirun -np 2. New:test_precision_floor.py(10 cases covering the floor at every consumption point, and its absence in float64), plus a reduction-accuracy regression intest_single_precision.pythat pins the relationship — whateversolve_rhsuses must track the double reference far more closely than a naive float32 dot — rather than an absolute error.End-to-end
simulate.py --precision singleruns clean at 32³, and the previously-bypassed path now reportsfixed cg-rtol 1e-06with a warning where it used to silently pin 1e-9.🤖 Generated with Claude Code