Repository navigation
GML part M5: one functor per layer family, the Bool keywords as type parameters, and the MHA head keys built once - #350
michakraus wants to merge 4 commits into
Conversation
…HA keys built once, the LinearSymplecticTransformer activation and upscaling_activation fixed, PositionalEncoding on the device
Q/P pairs share _shift; Bool ladders are one expression; LASympNet has one Chain method; GSympNet, LinearSymplecticTransformer and SymplecticTransformer share _gradient_layers (closes C19); MultiHeadAttention has one initialparameters with the draw order unchanged; ::Type{T} under src/layers; PositionalEncoding allocates on the input's backend (closes B11, K5). Pins of every touched architecture against 4c4a832 at 8 ULPs, forward inference of every exported architecture, and the MHA allocation equality.
…ts the default upscaling activation
…ayer encodes its bias as a Symbol, the ClassificationTransformer test separates its two flags
📝 Summary
Merge Risk: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments: 1
test/layers/type_arguments.jl: 1
22-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winScan only Julia source files recursively.
The current
src/layers/tree is flat, so this does not cause a current test failure. If a subdirectory is added,readdir(directory; join = true)passes that directory toeachline, and the test can error. A nested.jlfile would also be skipped. Use a recursive.jl-file list.Suggested fix
- hits = [(file, i) - for file in readdir(directory; join = true) - for (i, line) in enumerate(eachline(file)) if occursin(r"T::Type\b", line)] + files = [joinpath(root, f) for (root, _, fs) in walkdir(directory) + for f in fs if endswith(f, ".jl")] + hits = [(file, i) + for file in files + for (i, line) in enumerate(eachline(file)) if occursin(r"T::Type\b", line)]
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
76a76ebc-de0e-42e3-8b62-efb07f06060d
📒 Files selected for processing: 35
CHANGELOG.mdKNOWN_ISSUES.mdsrc/architectures/autoencoder.jlsrc/architectures/linear_symplectic_transformer.jlsrc/architectures/standard_transformer_integrator.jlsrc/architectures/symplectic_autoencoder.jlsrc/architectures/symplectic_transformer.jlsrc/architectures/sympnet.jlsrc/architectures/transformer_neural_network.jlsrc/layers/linear_symplectic_attention.jlsrc/layers/multi_head_attention.jlsrc/layers/positional_encoding.jlsrc/layers/psd_like_layer.jlsrc/layers/resnet.jlsrc/layers/symplectic_attention.jlsrc/layers/sympnets.jlsrc/layers/transformer.jlsrc/layers/volume_preserving_attention.jlsrc/layers/volume_preserving_feedforward.jltest/architectures/classification_transformer.jltest/architectures/forward_pins.jltest/architectures/gradient_layer_builders.jltest/architectures/linear_symplectic_transformer.jltest/architectures/standard_transformer_integrator.jltest/architectures/sympnet_integrator.jltest/helpers/architecture_pins.jltest/integration/documented_defaults.jltest/integration/forward_inference.jltest/layers/multi_head_attention_allocation.jltest/layers/multi_head_attention_key_order.jltest/layers/positional_encoding.jltest/layers/resnet_bias_encoding.jltest/layers/type_arguments.jltest/layers/volume_preserving_feedforward.jltest/runtests.jl
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
| DecoderType = last(_unknown_encoder_decoder(nn.architecture)) | ||
| NeuralNetwork( | ||
| UnknownDecoder(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_encoder_blocks), | ||
| DecoderType(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_encoder_blocks), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Pass n_decoder_blocks, not n_encoder_blocks, to the decoder architecture.
UnknownDecoder and UnknownSymplecticDecoder declare the field n_decoder_blocks. This line fills that field from nn.architecture.n_encoder_blocks. The block counts can differ. One example is SymplecticAutoencoder, which has 2 encoder blocks and 3 decoder blocks by default. For an autoencoder with different counts, the extracted decoder's n_decoder_blocks is wrong. The generic method now also handles every SymplecticCompression (for example PSDArch), so this wrong value reaches those architectures too.
Proposed fix
--- "a/src/architectures/autoencoder.jl"
+++ "b/src/architectures/autoencoder.jl"
@@ -233,7 +233,7 @@
function decoder(nn::NeuralNetwork{<:AutoEncoder})
DecoderType = last(_unknown_encoder_decoder(nn.architecture))
NeuralNetwork(
- DecoderType(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_encoder_blocks),
+ DecoderType(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_decoder_blocks),
decoder_model(nn.architecture),
decoder_parameters(nn),
networkbackend(nn))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| DecoderType(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_encoder_blocks), | |
| DecoderType(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_decoder_blocks), |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #350 +/- ##
==========================================
+ Coverage 86.72% 93.45% +6.72%
==========================================
Files 58 58
Lines 1341 1222 -119
==========================================
- Hits 1163 1142 -21
+ Misses 178 80 -98 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- Pins compare at 16 ULPs of the pinned vector's largest magnitude, and a failure prints the measured deviation (pin_ulps), as decided for M5. - ClassificationTransformer(dl) calls the dimension constructor, as in #350. - ActivationLayerQ/P and LinearLayerQ/P convert the dimension to Int, as in #349, so an Int32 dimension names the same layer. - Tests: the Stiefel branch of the MHA initialisation and its draw order; inference at 32 heads (layer and StandardTransformerIntegrator(32)); the alias test reads the source, because Julia drops an unused alias variable; the stale header of the MHA inference test; a narrower name for the Bool-ladder testset. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Closed in favour of #351, which is now the one build of part M5. #351 takes this PR's The branch |
Part M5 of the plan that simplifies GeometricOptimizers and GeometricMachineLearning for 0.9: the layers and the architectures.
What changes
qandpvariants of the SympNet layers, ofSymplecticAttentionand ofLinearSymplecticAttentionshare one functor each, through_shift(Val(C), z, f).LinearLayerQ/LinearLayerPlose their unused type parameter.GradientLayer.second_dimis a concreteInt.Boolladders.VolumePreservingAttention, the VPFF layer,SymplecticAttention,SymplecticAutoencoderandResNetLayerencode each choice as a Symbol type parameter, built in one expression. The publicBoolkeywords stay.VolumePreservingFeedForwardLayeris one struct with aTriangleparameter.VolumePreservingLowerLayerandVolumePreservingUpperLayerstay exported as aliases. The bias initialiser keyword isinit_bias.MultiHeadAttention. Oneinitialparameters. The head keys are built once, and the forward pass reads each head by its name, so a pass allocates no key.Chainmethod forLASympNet. One_gradient_layersforGSympNet,LinearSymplecticTransformerandSymplecticTransformer(closes known issue C19). TheDataLoaderconstructors delegate.GSympNet.actisGSympNet.activation. Oneencoder/decoderpath forSymplecticCompression.LinearSymplecticTransformer(dim, seq_length; activation = sin)no longer raises aMethodError. The fieldupsacling_activationofStandardTransformerIntegratorisupscaling_activation, and the firstDenseof its chain carries that activation.::Type{T}in every method undersrc/layers/.PositionalEncodingallocates its encoding on the backend of its input (closes known issues B11 and K5).The
CHANGELOG.mdentries under[Unreleased]give the breaking changes and their migrations.The
MultiHeadAttentiondraw order is unchanged (head by head; in each head, query, key, value), and no seeded pin moved.MultiHeadAttention(Stiefel,add_connection),ClassificationLayer(average) and AbstractNeuralNetworks'Denseare outside theBool-ladder item, as decided below: they encode other choices and keep theirBoolparameters.Tests
test/architectures/forward_pins.jlwithtest/helpers/architecture_pins.jl: the outputs of the touched architectures inFloat32andFloat64on dense,SymmetricMatrix, triangular,SkewSymMatrixand Stiefel leaves, against values recorded on the base commit4c4a8320, at 8 ULPs.test/integration/forward_inference.jl:@inferredon the forward pass of every exported architecture, in both precisions.test/layers/multi_head_attention_allocation.jlandtest/layers/multi_head_attention_key_order.jl: the head keys and the head order.test/layers/resnet_bias_encoding.jl,test/layers/type_arguments.jl,test/layers/volume_preserving_feedforward.jl,test/layers/positional_encoding.jl(JLArray under disallowed scalar indexing), and the architecture tests of the two bugs.Full suite at
23a9c74f, run locally: Julia 1.13.2 and Julia 1.12.7 each pass 99 testsets, 6602 pass, 1 broken (JET, tracked), 0 fail, 0 error. The Metal group passes 40/40 on both versions (Apple M4 Max). The doctests pass on 1.13.2.Critic
The critic's other defect 1 is answered by the full-suite run above.
Pre-PR verification
Fixed: none needed.
Unresolved: none.
Pre-existing, adjacent (nit):
MultiHeadAttentionreads the head count fromlength(ps.PQ), notd.n_heads. This is new in the diff.ActivationLayer,LinearLayerandSymplecticAttentionconstructors do not coerceMtoInt, whereGradientLayernow does.compute_output_of_mha.Checked and clean:
LASympNetChainmerge reproduces all four old layer sequences.MultiHeadAttentiondraw order is unchanged.Piracy.huntreturns nothing.code_typedon every touched layer method, Float32 and Float64, shows concrete return types and noAnycall.fatou lintis clean. TheMultiHeadAttentionforward pass allocates exactly the same bytes as the literal-key form (cold, 8 combinations × 5 repeats).PositionalEncodingallocates one matrix plus the sum.PositionalEncodingagrees with the host on a real Metal GPU (matrix, 3-tensor,(q, p), gradient). This supports closing B11..jlfiles formatted.Not checked:
Unresolved
MultiHeadAttention's forward pass takes the number of heads from its parameters,length(ps.PQ), not fromd.n_heads. Parameters with more heads than the layer run without an error whenadd_connection = false. Left as is: a change of code after the critic's PASS is not in this pull request.Known issues added
LagrangianNeuralNetworknames its activation fieldact, where every other architecture saysactivation.ClassificationLayer'sinitialparametersnames its initialiserinit_weight!, where every other layer saysinit_weight.Known issues closed: B11, K5, C19.
Advisor decisions
Decided (advisor, 2026-10-19, high): check 4's last sentence reads: "
encoder/decoderof aSymplecticAutoencoderkeepNonLinearSymplecticEncoder/NonLinearSymplecticDecoder, and those ofa
PSDArch(SymplecticCompression) keepUnknownSymplecticEncoder/UnknownSymplecticDecoder, ason the parent commit
4c4a8320." The branch's code stays as it is. The test asserts all threefamilies over
REAL_ELTYPES: theSymplecticAutoencoderpair, thePSDArchpair, and a test-localstruct PlainAE <: AutoEncoderthat givesUnknownEncoder/UnknownDecoder.Decided (advisor, 2026-10-19, medium): check 3's "equals" is
abs(measured - literal) ≤ 1024B,with k = 1000 forward passes inside one function-barrier call and
literal > 0as the control. Onerebuilt key costs at least 240 B per pass, so the mutant exceeds the tolerance by more than 230×.
Decided (advisor, 2026-10-20, high): check 2's "σ === tanh" reads "σ === Dense(4, 6, tanh).σ",
because AbstractNeuralNetworks'
DensestoresActivation(tanh) = TanhActivation()(ANNsrc/activation.jl:29,src/layers/dense.jl:5-8), so no tree can satisfy the literal. The testalso builds the architecture with the default
upscaling_activationandtransformer_dim = 6, andasserts
Chain(arch).layers[1].σ === Dense(4, 6, identity).σ. Tests catch:ChaincallingDense(arch.sys_dim, arch.transformer_dim)without the activation picksDense's defaulttanh,and the default-activation assertion fails. A constructor that drops the keyword stores
identity,and the
tanhassertion fails. Nothing changes in AbstractNeuralNetworks.Decided (advisor, 2026-10-20, high): the MHA item's "The RNG draw order changes; re-pin any
seeded test" and the edge "The RNG draw order" predict what the sketch in gml-simplify.md §4.4
would do. They do not require that change. The item requires one
initialparametersand the headkeys built once. A build that keeps the parent's draw order (head by head; in each head, query,
key, value) meets the item. It re-pins no seeded test, and the pull request says that the draw
order is unchanged and that no seeded pin moved. Tests catch: draws reordered to "PQ for every
head, then PK, then PV" in the one
initialparametersmake theStandardTransformerIntegratorpins (n_heads = 2, dense MHA leaves, test/architectures/forward_pins.jl) fail at 8 ULPs.
Decided (advisor, 2026-10-20, medium): the
Bool-ladder item reaches the four ladders itlists, and each other GML layer that encodes one of the same choices. In GML, only
ResNetLayerdoes, for the bias. "The same kind in every layer" means a Symbol type parameter in those layers.
ResNetLayerencodes its bias as:bias/:no_bias, as the VPFF layer does, and dispatches itsinitialparameters,parameterlengthand forward methods on that Symbol. Itsuse_bias::Boolkeyword stays.
MultiHeadAttention(Stiefel,add_connection) andClassificationLayer(
average) encode other choices, have no ladder, and keep theirBoolparameters. ANNDenseis outside M5, and the pull request names the three as out of the item. Tests catch: for
use_bias in (true, false),typeof(ResNetLayer(3; use_bias).parameters[3]) === typeof(VolumePreservingLowerLayer(3; use_bias).parameters[3]),and
ResNetLayer(3) isa ResNetLayer{3, 3, :bias, typeof(identity)}. The mutant that passesuse_biasthrough as aBoolfails both. InFloat32andFloat64,parameterlength(ResNetLayer(3; use_bias = false)) == 9and== 12with the bias.ResNetLayer(3, tanh; use_bias = false)givesx + tanh.(W * x), witheltype === T. Themutant that sends
:no_biasto the bias method fails both.Arbitrator decision
Decided (arbitrator, 2026-10-20, high; M5, second FAIL): Verify 2 failed at
fbea9d28on onefix regression. The comment that fix 1 added at
src/layers/resnet.jl:17sits between the@doc raw"""…"""andstruct ResNetLayer, so Julia attaches no docstring toResNetLayer. Thisgives the 7 unresolved references in
test/quality/docs_make_checks_references.jlon 1.13.2 and1.12.7. Fix 2 moves that comment inside the struct, or deletes it, and changes nothing else.
Verify 3 judges
fbea9d28..HEAD. It is the last critic round that runs without the user.Next round: (a) Change only
src/layers/resnet.jl:17: put the comment inside thestruct ResNetLayerbody aboveactivation::F1, or delete it. No other source line changes. (b) Beforeit reports, the builder runs
quality/docs_make_checks_references.jl(it must pass 6/6) andDocs.hasdoc(GeometricMachineLearning, :ResNetLayer). Fix 1 ran 29 files and left out thistest. (c) Test that catches the removal:
test/quality/docs_make_checks_references.jl:33,35.Input: the docs reference check. Assertion: exactly
1 unresolved reference(s)(the fixture'sown). Mutant M-doc puts a
#comment line back between the closing"""andstruct ResNetLayer. It must be CAUGHT, because the count goes to 7. (d) The 22 mutants of fix 1(R1–R7, F1–F5, N3a/b/k, O6, O9, O29, O34, O35) stay CAUGHT. (e) Verify 3 closes when (b) holds,
M-doc is CAUGHT, and the dispatcher's
fullsuite is green on 1.13.2 and on 1.12.7, withslow,metalanddoctestsrun. A FAIL at verify 3 stops the loop (triggerblocked).User decision
Decided (user, 2026-10-20): a recorded vector is compared at a stated count of ULPs of the
larger magnitude, not with
==: 8 ULPs here. The pins stay recorded from the parent commit; onlythe comparison changes. The CI runners showed the SAE encoder vector differing by 1 to 2 ULPs
between x86_64 and arm64, while a wrong extraction moves it by a fraction, thousands of ULPs away.