Keep a Float32 network Float32: training history, _norm, PoissonTensor defaults, the optimizer step size and two parameterlength counts - #295
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #295 +/- ##
==========================================
+ Coverage 80.61% 80.93% +0.32%
==========================================
Files 80 80
Lines 3162 3163 +1
==========================================
+ Hits 2549 2560 +11
+ Misses 613 603 -10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`(o::Optimizer)(nn, dl, batch, n_epochs, loss)` built its loss array with an
untyped `zeros(n_epochs)`, so a Float32 network's training history came back
as a Vector{Float64} even though `optimize_for_one_epoch!` accumulates the
loss in the data's own element type throughout. Type the array from
`eltype(dl)`, and assert (rather than convert) that each loss value already
has that type, so a future divergence fails loudly instead of silently
changing the accumulator's type.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The (q, p) arm divided by `√2` and the generic NamedTuple arm by `√length(dx)`, both Float64 literals that widened a Float32 sum to Float64; the plain-AbstractArray arm was already correct. This reaches users through `reduction_error` and `projection_error`, so a Float32 reduced-order model reported its error in Float64. Both NamedTuple arms now convert the divisor to the argument's own (promoted) element type before dividing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PoissonTensor(backend::Backend, n2::Int) defaulted to Float32 and PoissonTensor(backend::CPU, n2::Int) to Float64 -- an undocumented split added in one 2024 commit with no stated reason, so the CPU and GPU paths silently disagreed on the element type of a Poisson tensor built without one. Every real call site in scripts/, docs/ and test/ that names a backend already names a type too, so delete both constructors: a caller that names a backend now has to name a type as well. PoissonTensor(n2) (no backend at all) is unaffected and keeps its documented Float64 default -- the Julia convention, and the one every un-backended call site already relies on. Apple GPUs have no Float64 at all, so a Float64 default on a GPU backend would have been a silent regression; deleting the constructor rather than unifying on one type avoids having to pick a wrong default for either side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zeros(sys_dim, input_time_steps, n_params) had no element type, unlike the
three sibling DataLoader constructors in this file that all write
zeros(T, ...). Match them.
No test accompanies this one. This constructor dispatches on an
EnsembleSolution{T, T1, Vector{ST}} with ST a GeometricSolution whose
dataser NamedTuple has exactly the keys (:t, :q, :v) or (:t, :q, :q̇) -- and
after checking every equation type GeometricEquations 0.21 (the version
this package's Manifest resolves) ships, none produces an initialstate with
exactly those two keys: ODE gives (:q,) alone, and every type that has a
:v or :q̇ field (SODE, IODE, LODE, IDAE, LDAE) pairs it with additional
keys such as :p. GeometricEquations.EnsembleProblem also resolves the
`superType` type parameter via `eval(typeof(equ).name.name)` evaluated in
its own module's scope, which forecloses defining a matching equation type
from outside that module for testing purposes. This method therefore
appears unreachable with the current dependency versions -- worth a look
for Part E's dead-code sweep -- and I could not devise a runtime
reproduction for it within reasonable effort. The fix is still correct by
inspection: it is character-for-character what this file's three other
zeros(...) sites already do.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_leaf_optim_step! called GeometricOptimizers._rmul!(direction(cache), step_size) with the raw step_size, which the step-size funnel always hands over as a Float64 regardless of the parameters' element type -- unlike the three _euclidean_update! methods a few lines below, which all convert with T(step_size) first. A Float32 layer was therefore scaled at Float64 precision and only rounded back to Float32 on write: a different (and more expensive) answer than scaling in Float32 outright. Convert with T(step_size) here too, matching the sibling methods. _default_step_size's two literals are wrapped in Float64(...) explicitly now rather than left as bare exponent literals, matching the Float64 funnel they feed; the funnel itself stays in Float64 and converts at each point of use, which is the pattern the rest of this file already follows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
parameterlength(::PSDLayer{M,N}) and
parameterlength(::MultiHeadAttention{M,M,true}) both routed an integer
count through Float64 division and back through Int(...). The value does
not change for any size the rest of the suite constructs, but the old
Float64 path silently rounds to the wrong integer once the intermediate
product exceeds 2^53 -- verified against BigInt arithmetic at
M2 = 151_177_507 for PSDLayer and M = 83_767_764 for MultiHeadAttention,
both well inside Int64's own range. Both layers store nothing sized by M
or N, so parameterlength can be evaluated at a scale no real layer could
ever allocate; the new tests do exactly that. Rewritten with ÷ alone,
which is exact by construction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test/data_loader/batch_data_loader_qp_test.jl trains a Float32 GSympNet with FeedForwardLoss on (q, p)-shaped data, and the assert fired on every run: AbstractNeuralNetworks.FeedForwardLoss computes its loss through that package's own _norm(::NamedTuple) = sum(...) / √length(dx), which has the identical Float64-widening defect this branch fixes in GeometricMachineLearning's _norm (previous commit) -- but in a dependency, not in this package. A Float32 GSympNet's loss value is therefore genuinely Float64 today, upstream of anything this branch can reach. The assert converted that (previously harmless) upstream imprecision into a hard failure: total_error rebinding to Float64 mid-loop does not change what B1 actually requires, because loss_array is now typed and narrows each write back to T regardless. Keeping the typed zeros(eltype(dl), ...) and dropping the assert satisfies B1 without depending on a fix outside this repository. Worth reporting to AbstractNeuralNetworks; left alone here as out of scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ELOG.md average=true and average=false disagree on element type only in their own doctests, which hand the layer a hand-written Int weight and Int input directly, bypassing initialparameters. Checked with a real Float32 network on both a 2- and 3-axis input: both methods already return T. Decision is intended, not a defect -- an average is a division and must promote an all-Int input the way Base.mean does, a last-column selection need not, the way last does not -- so no code change and the doctests are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two-call invariance test only genuinely discriminates the B5 fix for a Float32 method: T(step_size) is a no-op when T is already Float64, so the Float64 combinations in the old for-loop passed identically before and after the fix and were dropped -- they checked nothing. The remaining Float32 combinations also do not reliably diverge pre-fix for an arbitrary random draw: Random.seed!(7) happened to make the two roundings coincide for Adam, though not for MomentumMethod, which is exactly the "test that passes both before and after proves nothing" failure the critic found. seed = 2 was checked individually, per method, against a pre-fix checkout (temporarily reverting the T(step_size) conversion at src/optimizers/optimizer.jl:310 and restoring it after): both Adam and MomentumMethod diverge by one Float32 ULP (2.9802322f-8) pre-fix and are exactly invariant post-fix. The comment's blanket "differ by a few ULPs for every combination" claim, which was false for 3 of its 4 cases, is corrected to describe what was actually verified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
957bfaa wrapped 1e-3 and 1e-2 in Float64(...) "to match the funnel they feed", but both literals are already Float64 -- the wrapper changes nothing and is noise. The step-size funnel is deliberately Float64 by design (the plan says so); the real fix in that commit was T(step_size) at the _leaf_optim_step! call site, which stays as it was. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
0247e6c left a comment beginning "There used to be ... here", narrating what the code was rather than what it is -- this tree's rule is that everything but CHANGELOG.md describes the code as it is now. The history (the 2024 commit, the undocumented split, why it was removed) is already recorded in CHANGELOG.md's "Removed (breaking)" section; the comment now just states the present constraint and points there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e test 7815b72 typed the zero-filled buffer in the three-key-shape DataLoader constructor but could not devise a runtime reproduction. This entry answers why, concretely: - Every equation type GeometricEquations 0.21.3 ships reconstructs its own ics from its own fixed field set inside initialstate(equ, t, ics, params), discarding anything else the caller passes -- verified directly for SODE, and by inspection for the rest -- so no equation type can produce the two-key (:q, :v)/(:q, :q̇) dataser shape this method dispatches on. A NamedTuple with the right keys, handed straight to EquationProblem/EnsembleProblem, does not survive the equation-specific initialstate call. - GeometricSolution and EnsembleSolution each define exactly one inner constructor, so Julia generates no default field-based constructor for either, and there is no supported way to build one directly. - Two low-level bypasses were tried and both crashed the Julia process with a segmentation fault: ccall(:jl_new_struct, ...) on the (mutable) GeometricSolution, and ccall(:jl_new_struct_uninit, ...) followed by setfield! on each field. Neither is a technique to build a test on. The fix itself is unchanged and stays: it is correct by inspection and this is not a reason to remove the method, which is Part E's business. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Commit 31d2ca9 fixed the same defect in src/arrays/poisson_tensor.jl's comment but missed five more in test files this branch itself added: test/arrays/poisson_tensor.jl:63, test/data_loader/training_history_eltype.jl:1, test/layers/psd_parameterlength.jl:1, test/optimizers/step_size_element_type.jl:1 and test/reduced_order_modeling/norm_eltype.jl:4 all opened with "used to", narrating pre-fix behaviour in past tense. Each now states what the test checks about the code as it is now and points to CHANGELOG.md's "Fixed" section for the history, the same treatment 31d2ca9 already applied. No test logic changed -- only the leading comment block in each file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The element-type fix computed promote_type(map(eltype, values(dx))...),
which yields an array type for a nested NamedTuple -- so _norm threw
MethodError: no method matching Vector{Float32}(::Int64) where it used
to return a number. Measured on (q = (a = ...,), p = (b = ...,)): main
gives 1.285065671879467, that version throws, and typeof(sum(map(norm,
dx))) gives 1.2850657 in Float32. The narrowing was not intended and
three reviews did not catch it.
PoissonTensor's docstring now lists all three constructors and states
that the element type defaults to Float64 and the backend to CPU().
The removal note called that default documented; it was not.
Five comments and two testset names rewritten from narrating the code
this branch replaced to stating what the test asserts, and one stale
optimizer.jl:310 reference dropped -- a line number is exactly what this
branch just shifted elsewhere.
Those shifts falsified nine references: test/aqua.jl's three piracy
witnesses and, in CHANGELOG.md's Open Issues, the same three plus add!
and the three PoissonTensor ambiguity witnesses. All re-measured and
corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`poisson_tensor.jl:39` is where `Base.getindex` sits on `main`. This branch adds three lines above it, so B8's witness for the nine `getindex` ambiguities pointed at a struct field. The sibling citation in the same entry was already re-pointed; this one was missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feef56d to
a07aad2
Compare
michakraus
left a comment
There was a problem hiding this comment.
Verdict: request changes — two file:line citations that the rebase invalidated are still wrong, and both sit in the part of the branch whose job is to keep citations pointing at the right line. Everything else in the diff verifies: the five element-type fixes, every number the tests quote, the ambiguity and piracy baselines, and all three conflict resolutions.
Reviewed at a07aad2e, which is the current PR head (gh pr view 295 --json headRefOid and git ls-remote origin refs/heads/fix/audit-part-b both give a07aad2e77b8987ae9eb6ac73774066cf8deea03). CI: all eleven jobs pending on that head at review time (run 35358964036), so no CI job was read. The test results below are local runs, named individually.
Findings
| # | severity | file:line | claim | evidence |
|---|---|---|---|---|
| 1 | bug | CHANGELOG.md:3153, CHANGELOG.md:3179 |
B10 cites src/data_loader/data_loader.jl:323 for the method and :337 for the fix. Both are off by two after the rebase; the real lines are 325 and 339. |
grep -n in the worktree: 325:function DataLoader(ensemble_solution::EnsembleSolution{T, T1, Vector{ST}}; and 339: data = zeros(T, sys_dim, input_time_steps, n_params). Pre-rebase they were 323/337; b7f3b66e on main ("Throw instead of returning nothing on an unexpected autoencoder keyword") added two lines above them. |
| 2 | bug | test/aqua.jl:39 |
Still cites poisson_tensor.jl:39 for getindex(::PoissonTensor, i, j). This branch moved that definition to line 42, and CHANGELOG.md:3126 was re-pointed to :42 — the two documents now state different lines for the same method. |
grep -n "Base.getindex" src/arrays/poisson_tensor.jl gives 42:. grep -n "poisson_tensor.jl:" test/aqua.jl CHANGELOG.md gives test/aqua.jl:39: … poisson_tensor.jl:39 against CHANGELOG.md:3126: … poisson_tensor.jl:42. |
| 3 | quality | CHANGELOG.md:655 |
Cites src/data_loader/data_loader.jl:367 for the EnsembleSolution constructor a script uses; it is at 369. Pre-existing — already stale on origin/main, from the same two-line shift as finding 1. It belongs in this branch because this branch is the one re-pointing citations into that file. |
git show origin/main:src/data_loader/data_loader.jl | grep -n "^function DataLoader(ensemble_solution" gives 325: and 369:. |
| 4 | bug | src/layers/grassmann_layer.jl:28 |
N > M ? (N - M)*M : (M - N):N — a colon where a * belongs. parameterlength(::GrassmannLayer) returns a UnitRange, not an integer, whenever N <= M. Pre-existing, but it is the third parameterlength of the set this branch audited and the only one that is outright broken. |
In a live session: parameterlength(GrassmannLayer(10, 4)) returns 6:5, of type UnitRange{Int64}. The sibling at src/layers/stiefel_layer.jl:20 already uses ÷ and is correct. |
| 5 | correctness-risk | src/data_loader/optimize.jl:95 |
zeros(eltype(dl), n_epochs) throws InexactError on the first write when eltype(dl) <: Integer, where zeros(n_epochs) did not. An integer DataLoader is constructible. |
eltype(DataLoader(rand(1:10, 4, 16); autoencoder = true)) == Int64. I could not reach the failure: the same training call dies earlier with MethodError: no method matching (::FeedForwardLoss)(::Chain{…}, …), because the loss rejects integer input. So no observed regression — worth a line in the entry rather than a code change. |
| 6 | nit | test/layers/psd_parameterlength.jl:18 |
The "ordinary sizes" testset recomputes expected with the same ÷ expression as src, so it cannot fail on an arithmetic error — only on an edit to the formula. MultiHeadAttention does have an independent cross-check; PSDLayer has none. |
test/architectures/check_parameterlengths.jl:22 derives the MHA count independently as 3*N÷n*Int(n*(N-(n+1)/2)), and passes under the rewrite (run below). No equivalent exists for PSDLayer. |
| 7 | nit | CHANGELOG.md, the _norm bullet |
The entry says the fix "reaches users through reduction_error and projection_error", but test/reduced_order_modeling/norm_eltype.jl only asserts _norm itself. The user-facing claim has no test. |
src/reduced_system/reduced_system.jl:217,218,237,238 are the four _norm call sites; no test asserts the element type of either error function. |
The three conflict resolutions — all correct
test/data_loader/runtests.jl— both@safetestsetblocks are present: main'sautoencoder_keyword.jland this branch'straining_history_eltype.jl. Both files exist, both are reached, both pass.test/reachability.jl, which walks theincludeclosure ofruntests.jl, passes with an empty allowlist.CHANGELOG.md, "Removed (breaking)" — main'slegacy/hnn/+legacy/mtk/entry and the branch'sPoissonTensorentry are both present, neither truncated.git diff origin/main...HEAD -- CHANGELOG.md | grep '^-'returns exactly six lines, every one of them an old citation replaced by the re-pointing. No prose was dropped.CHANGELOG.md, B8 — main's "eighteen … of the 23 this entry originally reported" text is kept and is internally consistent: 23 − 5 triaged = 18, and 17*pairs + 1Dense/Affine= 18. The re-pointed citations are right.src/arrays/poisson_tensor.jl:75,78,81are the threeBase.:*(::PoissonTensor{T}, …)methods (main had them at 71/74/77; this branch adds four lines above them), andpoisson_tensor.jl:42isBase.getindex. Both checked withgrep -nagainst HEAD and againstorigin/main.
Verified and fine
- Every other
file:linethe branch adds or shifts resolves.src/utils.jl:53isadd!,:65and:71the twoBase.:+pirate methods,:167theBase.:≈method. Main had 49/61/67/163; the_normrewrite adds four lines above them. Checked in bothCHANGELOG.mdandtest/aqua.jl.src/utils.jl:19-27(CHANGELOG.md:596) andsrc/optimizers/optimizer.jl:21(CHANGELOG.md:3109) are unshifted and still correct. - B10 is free. No
**B10.anywhere inCHANGELOG.mdonorigin/main, and a sweep of everyrefs/remotes/origin/*finds it only on this branch. The highest number on main is B9. The collision risk is forward-looking only: if thePositionalEncodingfollow-up allocates B10 independently, two different defects share one number. Reserve it now, or have the follow-up start at B11. - The baselines
test/aqua.jlasserts still hold, measured in a clean process.julia --project=test -e 'using GeometricMachineLearning, Aqua, Test; …'givesdetect_ambiguities(GML) = 18andpiracies = 12. That matches the@test length(Aqua.Piracy.hunt(…)) == 12gate and the "18 when GML is loaded alone" comment. Removing the twoPoissonTensorconstructors moved neither number. - Every number quoted in the new tests reproduces.
parameterlength(PSDLayer{302355014, 302355018})gives11427319538133785;BigIntarithmetic gives the same; the oldInt(M2 * (N2 - (M2 + 1) / 2))path gives11427319538133784— exactly as the comment says.parameterlengthof theM = 83767764, n = 6MultiHeadAttentiongives19296855159637518,BigIntagrees, and the old path gives19296855159637520. For the step-size guard, replaying_leaf_optim_step!with the pre-fix_rmul!(direction(cache), step_size)line separates the two arms by2.9802322f-8for bothAdam()andMomentumMethod(0.5)atseed = 2,step_size = 0.1— the figure the file's header claims. All four new tests are real regression guards. - The element-type fixes do what they say. Cold process,
--check-bounds=auto:_norm((q, p))and_norm((a, b))onFloat32arguments returnFloat64under the old expressions andFloat32under the new ones, with0allocations both before and after.eltype(PoissonTensor(4)) == Float64, andPoissonTensor(CPU(), 4)is aMethodError. T(step_size)matches the file's own pattern. The three_euclidean_update!methods atsrc/optimizers/optimizer.jl:259,267,282all convert first, and the funnel really does always hand aFloat64:_step_size(η::Real, ::Int) = Float64(η)at:204, theDecayingStaticarm at:209,_optimizer_step_size(η::Real) = Float64(η)at:213, and_step_size_from_linesearch(ls::Static) = Float64(ls.α)at:247.- The
PoissonTensorremoval breaks no call site.grep -rn "PoissonTensor(" src docs scripts test: every call that names a backend already names aT(test/arrays/poisson_tensor.jl:17, and the docstring's ownjldoctestatsrc/arrays/poisson_tensor.jl:26). The entry's claim holds verbatim. - The cross-package claims check out.
AbstractNeuralNetworks0.8's_norm(dx::NamedTuple) = sum(apply(norm, dx)) / √length(dx)(losses.jl:69in the resolved copy) does carry the identical defect.GeometricEquationsis at0.21.3in both manifests, andinitialstate(equ::SODE, t, ics, params)atsrc/odes/sode.jl:152does return(q = _statevariable(ics.q, periodicity(equ)),)alone, whileIODEatsrc/odes/iode.jl:188returns(q, p, v).ClassificationLayer's two doctests do showMatrix{Float64}atsrc/layers/classification.jl:43andMatrix{Int64}at:60. - Type stability and allocations.
Base.return_typesgivesFloat32for bothNamedTuplearms of_normandInt64for both rewrittenparameterlengthmethods.Base.eltype(::DataLoader{T}) where {T} = Tatsrc/data_loader/data_loader.jl:511, sozeros(eltype(dl), n_epochs)is inferable. No allocation change is attributable to the diff. - Tests run, and what they returned.
julia --project=testoverreachability.jlplus thearrays,attention,layers,optimizers,reduced_order_modelinganddata_loaderdrivers: exit 0, no failures, no errors, none broken. Notable summaries —Reachability of every test file2/2,Symplectic Potential (array tests)43/43 (which includes the two newPoissonTensorassertions),parameterlength(::MultiHeadAttention{M,M,true}) is exact4/4,parameterlength(::PSDLayer) is exact5/5,The GO-native leaf step scales in the parameter's own element type2/2,_norm keeps the element type of its argument6/6,A Float32 network's training history stays Float324/4. Separately,Check parameterlength9/9 — the independent cross-check on the MHA rewrite. - Hygiene. All nineteen changed
.jlfiles passJuliaFormatter.format(f; overwrite = false)under the repository'sscimlconfig. Every changed file,CHANGELOG.mdincluded, satisfiess == Unicode.normalize(s, :NFC). No new exported name and no new public API, so nothing to register in the docs; thePoissonTensordocstring gains three signatures and leaves thejldoctestbody untouched. - Comment tense. The new
srccomment atsrc/arrays/poisson_tensor.jl:60-63states an absence in the present tense and gives a design reason (Metal has noFloat64) rather than narrating a removal. The test headers describe counterfactuals, not history, and each counterfactual was reproduced above.
Not checked
- CI. Every job was still
pending. Nothing above reads a CI job, in either direction. - The docs build and the doctests.
Pkg.test()does not runjldoctestblocks, and I did not rundocs/make.jl. ThePoissonTensordoctest body is unchanged, so the risk is low, but it is unmeasured. TheDoctestsandDocumentationjobs are the check. - The full
Pkg.test(), and thelosses,transformers,kernels,activations,parametersanddocstringsdrivers. I ran the six drivers the diff touches, plusarchitectures/check_parameterlengths.jl. - Windows, macOS and the 1.10 floor. One local run, one platform. Nothing in the diff looks version-sensitive —
÷,typeof(n)(...)andzeros(T, n)are all long-standing constructs — but that is reading, not measuring. - B10's segfault paragraph. I did not attempt the two
ccallbypasses it reports, so "both crashed the Julia process" is taken on the entry's word. The reachability argument in front of it — that noGeometricEquations0.21.3 equation type yields a two-key(:q, :v)state — I did check directly forSODEandIODE. - ExplicitImports and a full
Aqua.test_all. The diff adds noimportorusingtosrc/, so neither is diff-attributable. I ran only the two checks that have asserted baselines.
What to change before merge
Findings 1 and 2 are one-line edits, and they are the whole of "request changes": point B10 at data_loader.jl:325 and :339, and bring test/aqua.jl:39 to poisson_tensor.jl:42 so it agrees with CHANGELOG.md:3126. Finding 3 is the same class and one more line. Finding 4 — grassmann_layer.jl:28 returning a UnitRange — is a real bug in the third member of the set this branch audited, and reads as belonging here rather than in a follow-up nobody opens.
Three stale `file:line` citations. The rebase onto main shifted
`data_loader.jl` by two lines, so B10's two witnesses pointed one function
short; `test/aqua.jl` still cited `poisson_tensor.jl:39` for `Base.getindex`
while the CHANGELOG had already been re-pointed to 42, so the two documents
disagreed about the same method. The third, at the `DataLoader` constructor,
was stale on main too.
`parameterlength(::GrassmannLayer{M, N})` returned a `UnitRange` whenever
`M >= N`: a colon stood where the product belongs, so `GrassmannLayer(10, 4)`
counted `6:5` rather than 24. It is the third member of the parameterlength set
this branch audits, and the only one wrong in kind rather than in rounding.
Nothing asserted it, so a test now does, deriving the Grassmann dimension
independently and checking the count is symmetric in the two sizes.
The training history is typed `float(eltype(dl))` rather than `eltype(dl)`. An
integer loader would otherwise get an integer history, and the first float loss
written into it would raise `InexactError` where the untyped `zeros` did not.
`float` is the identity on every floating-point type, so the Float32 case this
branch is about does not change.
The PSD count's "ordinary sizes" test restated the formula in `src`, which
detects change rather than checking correctness. It now sums the Stiefel
manifold's dimension term by term instead.
One CHANGELOG claim is qualified: the `_norm` fix does reach `reduction_error`
and `projection_error`, but only `_norm` itself is asserted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part B of a whole-package audit of
GeometricMachineLearningat v0.7.0: aFloat32network must stayFloat32. Six places did not. As with Part A,Pkg.test()is green onmainwith every one of them in place, so each fix lands with a test that fails before it and passes after, verified per test rather than per batch.The house pattern is already here —
src/loss/symplectic_euler_loss.jlandsrc/loss/variational_midpoint_loss.jlconvert the timestep toeltype(input)rather than promoting against it, andsrc/architectures/hamiltonian_neural_network.jlbuilds its Poisson tensor fromIntfor the same reason. This makes the rest of the package follow it.What is fixed
A
Float32network's training history came backVector{Float64}.optimize_for_one_epoch!was careful to accumulate inT, and then theOptimizerfunctor stored the result in an untypedzeros(n_epochs)._normwidened two of its three methods. The(q, p)arm divided by√2and the genericNamedTuplearm by√length, bothFloat64, while theAbstractArrayarm was already right. Three arms of one function disagreeing. It reaches users throughreduction_errorandprojection_error, so aFloat32reduced system reported aFloat64error.PoissonTensorcarried an undocumented CPU/GPU element-type split.PoissonTensor(backend, n2)defaulted toFloat32andPoissonTensor(CPU(), n2)toFloat64— added in one 2024 commit with no stated reason. Both are removed rather than unified: Apple GPUs have noFloat64at all (MtlArray(rand(Float64, 4))errors), so a shared default could only be wrong for one side. Every call site inscripts/,docs/andtest/that names a backend already names aT, so a caller now has to.PoissonTensor(n2)is unaffected and keeps itsFloat64default — which the docstring now actually states.An untyped
zerosin aDataLoaderconstructor, whose three sibling methods all wrotezeros(T, …).A
Float64step size reachedGeometricOptimizers._rmul!in the GO-native leaf step, where the three_euclidean_update!methods all convert withT(step_size). The funnel staysFloat64deliberately; the conversion belongs at the point of use.This one is not a style point.
rmul!(::MtlArray{Float32}, ::Float64)raisesInvalidIRErrorcompilingGPUArrays.gpu_rmul_kernel!, and with aFloat32scalar it returns normally — so GPU training did not run for any architecture. Confirmed on an Apple GPU: on this branch, aGSympNetonMetalBackend()trains two epochs and returns a finiteVector{Float32}with its parameters stillMtlMatrix{Float32}; the same call onmainraises the error. Reading the rest of_leaf_optim_step!confirms this was the onlyFloat64scalar reaching a kernel there.Two
parameterlengthfunctions routed integer counts throughFloat64division and back throughInt(...). Exact at the sizes in use, wrong beyond theFloat64mantissa. Rewritten with integer division:3M(M + n)is always divisible by2nbecausen_heads ∣ M, andM2(M2 + 1)is always even, so the rewrites are algebraically identical wherever the old ones were exact.Recorded, not fixed
src/layers/classification.jl'saverage = trueandaverage = falsemethods disagree on output element type; the decision and its reason are in the changelog. A new open issue B10 records that theDataLoader(::EnsembleSolution)method carrying thezerosfix appears unreachable: everyGeometricEquations0.21.3 equation type rebuilds its ownicsinsideinitialstate, so nothing produces the two-key(:q, :v)shape it dispatches on, and neitherGeometricSolutionnorEnsembleSolutionoffers a field-based constructor. The method is left in place — removing dead code is a separate change.Out of scope
src/arrays/poisson_tensor.jl'sBase.:*methods are untouched; they account for 17 of the 23 ambiguities and are a separate change.Pre-PR verification
Fixed during verification
_norm(::NamedTuple)no longer throws on a nestedNamedTuple. The element-type fix had usedpromote_type(map(eltype, values(dx))...), which yields an array type for a nested argument, soT(length(dx))becameVector{Float32}(2). Measured on(q = (a = …,), p = (b = …,)):mainreturns1.285065671879467, that version raisesMethodError: no method matching Vector{Float32}(::Int64), andtypeof(sum(map(norm, dx)))returns1.2850657. Same element type, same value, generality restored.PoissonTensor's docstring now lists all three constructors and states that the element type defaults toFloat64and the backend toCPU().optimizer.jl:310reference dropped — a line number is exactly what this branch shifts elsewhere.test/aqua.jl's three piracy witnesses, and in## Open Issuesthose three plusadd!and the threePoissonTensorambiguity witnesses. All re-measured.Checked and clean
PoissonTensor(MetalBackend(), 4, Float32)works,PoissonTensor(MetalBackend(), 4)is now aMethodError,PoissonTensor(4)isFloat64on the host. No backend can default to a type Metal cannot hold, and no two-argument backend call site remains anywhere.Aqua.Piracy.hunt→ 12, matching the gate; none in a changed line._normreturnsFloat32on all three arms with noAny; bothparameterlengthmethods returnInt64;_leaf_optim_step!infersNothingover 807 statements with noAnyorBox; theOptimizerfunctor now infersVector{Float32}.--check-bounds=auto:_normallocates 0 bytes before and after, on both arms. No hunk adds an allocating construct.parameterlengthvalues re-derived againstBigInt: MHA19296855159637518against theFloat64path's19296855159637520, PSD11427319538133785against11427319538133784.jldoctestblocks in the changed files re-run by hand, output unchanged. JuliaFormatter clean on every changed file. NFC clean.main, 2 pre-existingbroken, 0 failures.Pre-existing, next to the diff — not fixed
src/optimizers/optimizer.jl:275andsrc/utils.jl:58narrate history outside the changelog.fatou lint:typeof(f) == NothingFunctionatsrc/utils.jl:4should be anisatest.Not checked
docs/make.jlbuild. Thejldoctestblocks in the changed files were re-run individually instead.🤖 Generated with Claude Code