Repository navigation
Train on GeometricOptimizers 0.9: NetworkOptimizer and train! (part M1a) - #342
Conversation
… train! (part M1a) NetworkOptimizer wraps GO 0.9's TrainingOptimizer over the whole parameter set; train!(nn, opt, dl, batch, n_epochs; loss, show_progress) replaces the Optimizer functors and optimize_for_one_epoch!. The aliases, the per-layer caches and the hand-built step (about 400 lines) go. Compat: GO 0.9, ANN 0.9.1, NNP 0.5, SNN 0.9, GeometricBase 0.15, GeometricEquations 0.21.5, GeometricSolutions 0.6.6. The methods moved to ANN (3-tensor Dense/Linear functors, HDF5 save/load and the extension) go; the call sites in tests, docs, README and scripts move, and the CHANGELOG and KNOWN_ISSUES entries follow. GO 0.9's triangular types keep their old GML names here, through an aliased import; the next commit renames them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
StrictlyLowerTriangular and StrictlyUpperTriangular replace the two names in src, test, docs/src and scripts, and GML re-exports them under the new names. The CHANGELOG names the rename. Co-Authored-By: Claude Opus 5.5 <[email protected]>
K10: two Adam training tests run in Float64 only. K11: a test comment reports a past experiment. K12: Batch never draws the last sample. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Comments in src/GeometricMachineLearning.jl, test/quality/aqua.jl and scripts/verification/all_zero_leaf.jl; KNOWN_ISSUES.md entries under the heading of their kind; the GPU paragraph of docs/src/index.md no longer reports training with the removed Optimizer, so K9 goes; the CHANGELOG zero-leaf sentence names every method that carries momentum. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The CHANGELOG entry names the known issues C1, C2 and D4 that this change closes; the all-zero-leaf script header, the B6 note and the GPU paragraph's compat scope match the code. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #342 +/- ##
==========================================
+ Coverage 86.64% 88.49% +1.84%
==========================================
Files 76 74 -2
Lines 2786 2633 -153
==========================================
- Hits 2414 2330 -84
+ Misses 372 303 -69 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ration - scripts/verification/all_zero_leaf.jl imports optimization_step! through GeometricMachineLearning, as sympnet_pendulum.jl does: the scripts environment that runscripts.jl uses has no GeometricOptimizers entry. The header names the scripts environment. - docs/inventories/GeometricOptimizers.toml is regenerated from the deployed v0.9.0 objects.inv, the version [compat] requires, with the command in docs/make.jl, which now pins v0.9.0. The 0.5.0 copy has no StrictlyLowerTriangular or StrictlyUpperTriangular, so the eight @extref links to them failed when the build fell back to it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ts owner in scripts (part M1a, fix 3) The second half of the testset in test/optimizers/optimization_step.jl steps 64 x 64 leaves and compares the two steps within 256 bytes. Julia min on Windows read 48 bytes apart at the 3 x 3 leaves, above the old 32-byte tolerance. A deepcopy of the gradient now costs 17 664 B (Float32) and 34 336 B (Float64), and a temporary of the first leaf 16 464 B and 32 848 B; both mutants are caught in both precisions. scripts/Project.toml gains GeometricOptimizers, and all_zero_leaf.jl and sympnet_pendulum.jl import optimization_step! from it, not through GeometricMachineLearning, whose binding is not public. Co-Authored-By: Claude Opus 5.5 <[email protected]>
michakraus
left a comment
There was a problem hiding this comment.
Review of 826ae32
Verdict: request changes (posted as a comment review). A required check is red because of a test that this PR adds. The new train! takes 5 to 8 times longer than the code it replaces to compile its first call on a transformer. That makes the Scripts job time out.
CI, read per job
| job | required? | caused by this PR? | red on main? |
what the log says |
|---|---|---|---|---|
| Julia min - windows-latest - default | yes (branch protection and ruleset) | yes: the failing test is new here | no (main run 37980189635: success) |
test/optimizers/optimization_step.jl:80, Expression: abs(allocations(optimization_step!, ps, o, dp) - allocations(optimization_step!, ps, o.optimizer, dp)) ≤ 32, Evaluated: 48 ≤ 32; 731 passed, 1 failed |
| Scripts - ubuntu-latest | no | yes: see finding 2 | no (main run 37980189875: all 26 scripts ok) |
TIMEOUT 300.1s reproduction/symplectic_transformer/double_pendulum.jl, TIMEOUT 300.2s …/double_pendulum_short_integration.jl, TIMEOUT 300.1s …/symplectic_transformer_vector_softmax.jl, TIMEOUT 300.2s reproduction/volume_preserving_transformer/rigid_body.jl |
| Downgrade - ubuntu-latest | no | no | yes, the same job is red on main (run 37980189635; the workflow conclusion reads success) |
forcedeps check failed: GPUArraysCore resolved to 0.2.1 but lower bound is 0.2.0, the same for Zygote, ChainRulesCore, NNlib and KernelAbstractions, as on main. On main GeometricBase also failed. This PR fixes that one by raising the bound to 0.15 |
The Scripts job on main is three days old. The attribution does not rest on it: finding 2 compares the base with this PR, and the shipped train! with a rewrite, on one machine.
Findings
| # | severity | file:line | claim | evidence |
|---|---|---|---|---|
| 1 | blocker | test/optimizers/optimization_step.jl:80-81 |
The Stiefel half of "allocates nothing of its own" requires two separate @allocated readings of a step to agree within 32 bytes. That step allocates 2 to 4 kB. The bound fails on Windows only, so this test turns a required check red. |
The CI log above (48 ≤ 32 on Windows, Julia 1.12.7). On macOS, with --check-bounds=yes, the difference is 0 in 90 of 90 trials per element type on both 1.12.7 and 1.13.1 (Float32 step 2320 B, Float64 3520 B). The thing under test is the one-line forward at src/optimizers/optimizer.jl:33-35. The Euclidean == 0 at :71 already shows that it allocates nothing. |
| 2 | bug | src/data_loader/train.jl:33-37 with :41-45 |
The map(1:n_epochs) do … end around _train_epoch!, which itself holds sum(generator), makes the first train! call compile for 91–98 s. The old functor takes 18.4 s and a plain loop takes 11.2 s. The shipped form and the plain loop give the same history. All six reproduction scripts that slowed or timed out in the Scripts job train transformers for 2 epochs (the smoke size), so their run time is mostly compilation. Two of the six went from 131.7 to 289.3 s and from 145.2 to 293.7 s. |
A fresh process for each run, Julia 1.13.1, StandardTransformerIntegrator(4; transformer_dim = 4, n_heads = 1, L = 1, n_blocks = 4, attention_activation = MatrixSoftmax()), Batch(64, 10), 2 epochs, show_progress = false. Base o(nn, dl, batch, 2): 18.4 s. This PR's train!: 98.1, 96.7, 91.0 and 93.5 s. The form below: 11.2 and 11.2 s. History [2.315492636526417, 2.172620338370672] from both PR forms. The parts, measured one at a time: the outer map with a plain inner loop takes 15.2 s, and an outer loop around the shipped _train_epoch! takes 11.2 s. So it is the nesting of the two that costs the time. The load average was 7–18, but the base and PR runs alternated, so the load affected both. |
| 3 | quality | src/data_loader/train.jl:31 |
train! always uses ZygotePullback(loss). The package still exports AbstractPullback and SymbolicPullback (src/GeometricMachineLearning.jl:250), and docs/src/architectures/hamiltonian_neural_network.md presents SymbolicPullback as the alternative gradient for HNNs. Nothing can train with it now. One keyword gives that back. |
Read. On main, the functor took _pullback::AbstractPullback = ZygotePullback(loss) (src/data_loader/optimize.jl:85). The CHANGELOG records the removal, but not the fact that the exported pullback is left with no use. |
| 4 | quality | test/optimizers/step_size_element_type.jl (whole file), test/optimizers/gml_gradient_dispatch.jl:20-35 |
These tests test GeometricOptimizers, not this package. One tests the precision conversion of TrainingOptimizer's linesearch, and the other tests PrecomputedGradient(ps, dp)(ps). The only GML code that they reach is the keyword forward in NetworkOptimizer. These tests belong in GeometricOptimizers' suite. The file name gml_gradient_dispatch.jl names the _GMLGradient that this PR deletes. |
Read. Neither testset calls a GML function except NetworkOptimizer(ps; kwargs...). |
| 5 | quality | scripts/verification/all_zero_leaf.jl:8-13, :71-79 |
The header states results: a leaf that the loss never reads stays == its start under three methods, and momentum carries a leaf that the loss stops reading. The script only prints figures, so nothing fails when a claim stops being true, and the CHANGELOG cites this script as the evidence. @assert r.never_euclidean == 0 && r.never_stiefel == 0 for GradientMethod, MomentumMethod and Adam, and @assert after_reading(T, m) > 0 for the three momentum methods, make it a check. Also, one = p -> … at :60 shadows Base.one. |
Read. |
| 6 | quality (an option, for the author to decide) | src/optimizers/optimizer.jl:24-35 |
NetworkOptimizer is a wrapper with one field. Its only work is the nn constructor and the forward of optimization_step!. GeometricOptimizers' TrainingOptimizer(x::OptimizerSolution{T}; …) already takes a NetworkParameters. The type can go: train!(nn, opt::TrainingOptimizer, …), with TrainingOptimizer re-exported, and callers write TrainingOptimizer(params(nn); algorithm = Adam()). This removes one public type and the .optimizer indirection, which the tests write 17 times. If you want the nn form, keep the wrapper, because TrainingOptimizer(::NeuralNetwork) would be type piracy. |
Read: GeometricOptimizers 0.9.0 src/optimizers/training_optimizer.jl, TrainingOptimizer(x::OptimizerSolution{T}; algorithm::FirstOrderMethod = Adam(), linesearch = default_step_size(algorithm), …). |
| 7 | nit | src/data_loader/train.jl:54-56 |
_default_loss has no fallback. If you call train! without loss on any other network, the error names an internal function. A one-line fallback that throws an ArgumentError which tells the caller to pass loss matches the docstring ("any other network needs loss"). |
train!(nn, NetworkOptimizer(nn), dl, Batch(5), 1; show_progress = false) with nn = NeuralNetwork(Chain(Dense(2, 2, identity))) gives MethodError: no method matching _default_loss(::AbstractNeuralNetworks.UnknownArchitecture, ::Batch{:FeedForward}). |
| 8 | nit | src/kernels/kernel_ad_routines/mat_tensor_mul.jl:76-78, :148-150; src/kernels/mat_tensor_mul.jl:161-163, :209-211 |
When the formatter re-wrapped the rename, it split AbstractArray{T, 3} across two lines (A::AbstractArray{ / T, 3}) where {T}). Join them by hand. Also, the docstrings at :155-158 and :203-206 still say "lower-triangular" and "upper-triangular", not strictly. |
Read. |
| 9 | nit | test/data_loader/train.jl:44, test/optimizers/optimizer.jl:49-50, test/optimizers/optimization_step.jl:45-47 |
The tests read GeometricOptimizers' struct fields (opt.optimizer.state.iterations, .method, .linesearch, .cache). This ties GML's suite to GeometricOptimizers' internal layout. |
Read. |
| 10 | nit | CHANGELOG.md:20, docs/src/index.md:55 |
The first Removed (breaking) bullet is one paragraph of 400 words. It mixes the migration guide, notes on behaviour and the bookkeeping of known issues. Split it into one bullet per migration. The GPU paragraph reports measurements against GeometricOptimizers 0.8.0, which the package can no longer load. Delete those measurements or measure again. | sed -n 20p CHANGELOG.md | wc -w gives 400. |
| 11 | nit | PR body | The body says that "The commits after e8ff170 change no executable line". But 826ae32 changes scripts/verification/all_zero_leaf.jl:21 and docs/make.jl. The suite totals are for e8ff170, not for the head, and at the head the Julia min - windows job fails. The critic and advisor tables are claims. This review did not use them as evidence. |
gh pr view 342 --json commits; the CI log above. |
The simpler code for finding 2
This form also takes in finding 3. It is measured cold at 11.2 s, against 91–98 s for the shipped form, and gives the same history. A Float32 loss still gives a Vector{Float32}, because the vector takes the type of the first epoch's value:
function train!(nn::NeuralNetwork, opt::NetworkOptimizer, dl::DataLoader, batch::Batch,
n_epochs::Integer; loss::NetworkLoss = _default_loss(nn.architecture, batch),
pullback::AbstractPullback = ZygotePullback(loss), show_progress::Bool = true)
n_epochs ≥ 1 ||
throw(ArgumentError("`train!` needs at least one epoch; `n_epochs` is $(n_epochs)."))
progress = ProgressMeter.Progress(n_epochs; enabled = show_progress)
history = [_train_epoch!(nn, opt, dl, batch, pullback, progress)]
for _ in 2:n_epochs
push!(history, _train_epoch!(nn, opt, dl, batch, pullback, progress))
end
history
end
function _train_epoch!(nn, opt, dl, batch, pullback, progress)
batches = batch(dl)
value = sum(_train_step!(nn, opt, dl, batch, pullback, indices) for indices in batches) /
length(batches)
ProgressMeter.next!(progress; showvalues = [(:TrainingLoss, value)])
value
endFor finding 1, delete test/optimizers/optimization_step.jl:80-81. Keep the @inferred at :79 and the type check at :82. If you keep a bound on the Stiefel step, make it a tolerance well above the 16-byte steps in which Windows measures allocations, for example ≤ 256. Do not use a bound smaller than the noise.
Verified and fine
src/optimizers/optimizer.jl: the docstring defaults (algorithm = Adam(),linesearch = default_step_size(algorithm),retraction = Cayley()) are the same as those of GeometricOptimizers 0.9.0'sTrainingOptimizer.- The deletion of
src/data_loader/optimize.jl, the per-layer caches and_GMLGradient:git grepfinds no remaining use ofOptimizer(,optimize_for_one_epoch!or the three aliases insrc,docs/src,scripts,testorREADME.md, except the absence test intest/quality/exports.jl:37. - The warm run time per call is unchanged: the second 2-epoch call takes 0.01 s on both trees.
KernelAbstractions.get_backend(::PoissonTensor)extends a function of another package on GML's own type, so it is not piracy.Project.toml: the Downgrade forcedeps failures do not come from this PR, and the new lower bounds (GeometricOptimizers 0.9.0, AbstractNeuralNetworks 0.9.1, NeuralNetworkParameters 0.5.0, SymbolicNeuralNetworks 0.9.0, GeometricBase 0.15.0, GeometricEquations 0.21.5, GeometricSolutions 0.6.6) all resolve, as the forcedeps step shows.closingIssuesReferencesis empty. The body writes "relates to #145".
Not checked
- Whether finding 1 always happens on Windows or only sometimes. There is no Windows host here.
- Whether
SymbolicPullback's pullback output goes through_get_contentsunchanged (finding 3). - The full test suite, the docs build and the doctests. CI ran them, and they are green apart from the job above.
- Training on a GPU.
The map(1:n_epochs) do block around _train_epoch! made the first train! of a transformer compile for about 97 s: StandardTransformerIntegrator with 4 blocks, Batch(64, 10), 2 epochs, Julia 1.13.1, fresh process. The loop takes 12.2 to 21.9 s in three fresh processes and gives the same history. This is the cause of the four Scripts timeouts at 300 s (review finding 2). The progress meter now advances in _train_epoch!. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…any pullback Review findings 3 to 7 and 9 of #342. - The NetworkOptimizer wrapper is removed. train! takes a TrainingOptimizer, which GML re-exports, built with TrainingOptimizer(nn.params; ...). The optimizer re-exports move to src/optimizers/optimizer.jl. Tests, docs, tutorials, scripts and the README follow. - train! has a pullback keyword, ZygotePullback(loss) by default, so a Hamiltonian network trains with SymbolicPullback again. The new testset checks that one epoch moves the parameters by -eta times the symbolic gradient. Its comparison with Zygote is @test_broken: the symbolic gradient is the gradient of the per-sample sum, not of HNNLoss, on main too (#343, KNOWN_ISSUES K13). - _default_loss has a fallback that raises an ArgumentError asking for loss. - test/optimizers/step_size_element_type.jl and the PrecomputedGradient testset of gml_gradient_dispatch.jl are deleted; they test GeometricOptimizers and move to its suite. The _get_contents testset moves to test/pullbacks/get_contents.jl. - The Stiefel allocation comparison in optimization_step.jl compared the wrapper with what it wrapped; without the wrapper it is removed, and the Euclidean == 0 bound and the @inferred checks stay. - scripts/verification/all_zero_leaf.jl asserts its claims, and no longer shadows Base.one. - The tests read opt.state, opt.cache, opt.method and opt.linesearch directly: GeometricOptimizers 0.9 has no public accessor for them (iteration_number, solution and the cache are not public). - CHANGELOG: the 400-word bullet is split, and the entry names TrainingOptimizer and the pullback keyword. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…n the docstrings Review finding 8 of #342: the formatter had split AbstractArray{T, 3} over two lines in the four StrictlyLower/UpperTriangular methods, and their docstrings said lower- and upper-triangular. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review finding 10 of #342. Every claim of the GPU Support paragraph was run again on an Apple M4 Max with Metal.jl 1.11.1 and GeometricOptimizers 0.9.0: the eleven architectures construct, apply to a matrix and a 3-tensor, and copy back with map_to_cpu; the StrictlyLowerTriangular tensor product runs; and train! now trains a GSympNet for two epochs on the device. ClassificationTransformer was not tried, and the paragraph says so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolution of the review findingsThe review was on
Full suite on |
Summary
Training moves onto GeometricOptimizers 0.9's
TrainingOptimizer(part M1a of the 0.9 simplification of GeometricOptimizers and GeometricMachineLearning).Optimizerand its callable methods are removed.train!takes GeometricOptimizers'TrainingOptimizer, which GML re-exports and which is built on the parameters of a network:TrainingOptimizer(nn.params; algorithm, linesearch, retraction). One cache and one state cover the whole parameter set. There is no GML wrapper type:src/optimizers/optimizer.jlholds only the re-exports.train!(nn, opt, dl, batch, n_epochs; loss, pullback = ZygotePullback(loss), show_progress)insrc/data_loader/train.jlreplacessrc/data_loader/optimize.jl: the four callable methods,optimize_for_one_epoch!as a public name, andλYgo.pullbacktakes anyAbstractPullback, so a Hamiltonian network trains withSymbolicPullback(arch). The history has the element type of the loss value.n_epochs < 1is anArgumentErrorbefore any step, and so is a network with no default loss whenlossis not given. A second call with the sameoptresumes the first. The epoch loop is a plain loop: amapwith a closure made the first call of a transformer compile several times as long, which timed out four reproduction scripts in CI.GradientOptimizer,MomentumOptimizerandAdamOptimizerare removed. GML re-exportsTrainingOptimizer,GradientMethod,MomentumMethod,Adam,AdamWithEuclideanDecay,AdamOptimizerWithDecay,DecayingStatic,CayleyandGeodesic.optimization_step!is not re-exported; a hand-written loop callsGeometricOptimizers.optimization_step!(nn.params, opt, dp).[compat]: GeometricOptimizers"0.9", AbstractNeuralNetworks"0.9.1", NeuralNetworkParameters"0.5", SymbolicNeuralNetworks"0.9", GeometricBase"0.15", GeometricEquations"0.21.5", GeometricSolutions"0.6.6".Dense/Linearfunctors and the HDF5save/loadmethods move to AbstractNeuralNetworks 0.9.ext/HDF5Ext.jland the HDF5 weak dependency go. Aqua'spiraciescheck runs with 0 piracies, and the ambiguity count is 0. This relates to Type piracy #145 and closes known issue B7.LowerTriangular/UpperTriangularbecomeStrictlyLowerTriangular/StrictlyUpperTriangular, in a separate mechanical commit.train!andTrainingOptimizer. New and rewritten testsets loop overREAL_ELTYPES(Float32,Float64).scripts/verification/all_zero_leaf.jlasserts what a step does to a leaf that the loss does not read.train!now runs on the device too.Relates to JuliaGNI/AbstractNeuralNetworks.jl#15: the resume test (two runs of 5 epochs equal one run of 10) covers it within one process. The issue stays open for a decision by hand.
Commits
92ad227the API, with the old triangular names kept through an aliased import;e8ff170the rename only, with its CHANGELOG entry.274a7e0,68c47ff,69829d2KNOWN_ISSUES entries and text fixes from the critic and the pre-PR verifier.826ae32fixes of the Scripts and Documentation CI failures; it changesscripts/verification/all_zero_leaf.jlanddocs/make.jl.3529019scripts importoptimization_step!from GeometricOptimizers, and a wider allocation test (superseded by93409b0, see below).1d661a7,93409b0,adac904,20da228the fixes of the review below.Review findings and what changed
The review on
826ae32had 11 findings; each is resolved in the comment that accompanies this update. In short:Julia min - windows-latestjob failed on a 32-byte bound between two@allocatedreadings of a Stiefel step: one through the wrapper and one direct. With the wrapper removed, that comparison measured a step against itself, so it is gone; the Euclidean== 0bound and the@inferredchecks stay.train!of a transformer took about 97 s to compile with themaploop and 12–22 s with the plain loop, in fresh processes on Julia 1.13.1, with the same history. The four scripts that timed out at 300 s in CI run in 63–96 s locally.SymbolicPullbacktrains again through thepullbackkeyword. Its gradient is the gradient of the per-sample sum of losses, not ofHNNLoss; this is older than this PR (same numbers onmain), is filed as SymbolicPullback(arch) gives the gradient of the per-sample sum, not of HNNLoss #343 and recorded as K13, and the new test marks its comparison with Zygote@test_broken.test/optimizers/step_size_element_type.jland thePrecomputedGradienttestset ofgml_gradient_dispatch.jltested GeometricOptimizers, not this package; they are deleted here and move to GeometricOptimizers' suite. The_get_contentstestset moves totest/pullbacks/get_contents.jl.Test runs
20da228(the head), Julia 1.13.1,Pkg.test()with the default groups: Pass 4928, Fail 0, Error 0, Broken 6. The broken results: JET 3 of 145 (JET reports a runtime dispatch in PoissonTensor and in one batch method #325), Metal 1 (no GPU in the sandbox), and 2 intrain!'sSymbolicPullbacktestset (SymbolicPullback(arch) gives the gradient of the per-sample sum, not of HNNLoss #343).docs/check_references.jl: 0 unresolved references.scripts/verification/all_zero_leaf.jl: every assertion holds inFloat32andFloat64.Not run locally: the full docs build (the Documentation workflow runs it), and the suite on Julia 1.12 and on Windows.
Known issues added
SymmetricMatrixleaf with respect to aNetworkParametersdiffers from the one with respect to the bareNamedTuple(relates to Zygote gradient w.r.t. NetworkParameters loses the SymmetricMatrix leaf on a second access #319).mat_tensor_mulwhen the data has another element type.DataLoaderof one sample cannot be batched.adam_with_learning_rate_decay.jlrun inFloat64only. Pre-existing.adam_with_learning_rate_decay.jlreports a past experiment. Pre-existing.Batchnever draws the last sample of aDataLoaderof matrices. Pre-existing.SymbolicPullback(arch)gives the gradient of the per-sample sum, not ofHNNLoss(SymbolicPullback(arch) gives the gradient of the per-sample sum, not of HNNLoss #343). Pre-existing.Removed: B7, C1, C2, D4 (closed by this change). Changed: C16, K1.
Unresolved
train!infers its return as an abstractVector, becauseZygotePullback's call is annotated::Tupleand its loss value infers asAny; at run time the history isVector{L}, and the tests assert it. The pullback is the next part's.opt.state,opt.cache,opt.methodandopt.linesearchdirectly: GeometricOptimizers 0.9 has no public accessor for them.Pre-existing
KNOWN_ISSUES.mdsay "by this release" for B1, B2, B6, B8 and B9, which closed in 0.8.0.AdamOptimizerWithDecay, whosencounts minibatch steps. The behaviour is unchanged.🤖 Generated with Claude Code