Skip to content

GML part M5: layers and architectures share one method per Q/P pair, Bool choice and triangle - #351

Open
michakraus wants to merge 5 commits into
mainfrom
simplify/part-m5-3
Open

michakraus wants to merge 5 commits into
mainfrom
simplify/part-m5-3

Conversation

@michakraus

Copy link
Copy Markdown
Member

Summary

Part M5 of the GeometricMachineLearning 0.9 simplification: the layers and architectures share one method where a pair differed in one choice, two bugs are fixed, the multi-head attention builds its head keys once, and PositionalEncoding allocates on the backend of its input. The outputs do not change.

  • Q/P pairs. One function, _shift(Val(C), z, f), adds f of one half of a (q, p) input to the other half: src/layers/sympnets.jl has 3 forward methods on a (q, p) input where it had 6, symplectic_attention.jl 2 where it had 4, linear_symplectic_attention.jl 1 where it had 2. Each of the three SympNet forward methods keeps the layer's dimension check. GradientLayer.second_dim is an Int, and the aliases LinearLayerQ/LinearLayerP no longer name an unused parameter.
  • Bool ladders. Each Bool keyword of VolumePreservingAttention, the two volume-preserving feedforward layers, SymplecticAttentionQ/P, SymplecticAutoencoder and ResNetLayer picks a Symbol type parameter in one expression. ResNetLayer encodes its bias as :bias/:no_bias, as the volume-preserving layers do; its use_bias::Bool keyword stays.
  • Volume-preserving feedforward. VolumePreservingLowerLayer and VolumePreservingUpperLayer are aliases of one struct VolumePreservingFeedForwardLayer{M, N, bias, AT, Triangle}, with one initialparameters; the bias keyword is init_bias in every layer.
  • MultiHeadAttention. One initialparameters for both weight kinds. The forward pass finds each head by its name head_i, from a key tuple that a generated function builds once for each number of heads, where it built Symbol("head_", i) on every pass.
  • Architectures. LASympNet has one Chain method where it had four; one _gradient_layers builds the gradient layers of GSympNet, LinearSymplecticTransformer and SymplecticTransformer and replaces the two _make_block_for_initialization methods (known issue C19 leaves KNOWN_ISSUES.md); the DataLoader constructors of LASympNet and GSympNet delegate; GSympNet names its activation field activation; one encoder and one decoder serve every AutoEncoder that is not a SymplecticAutoencoder.
  • Bugs. LinearSymplecticTransformer keeps its activation (it built new{typeof(tanh)}, so activation = sin raised a MethodError). StandardTransformerIntegrator's field upsacling_activation is spelled upscaling_activation.
  • ::Type{T} everywhere under src/layers/.
  • PositionalEncoding fills similar(x, eltype(x), dim, seq_length) by one broadcast, so it runs on a JLArray with scalar indexing disallowed; the "CPU only" docstring warnings go (known issues B11 and K5 leave KNOWN_ISSUES.md).

Breaking changes are listed in CHANGELOG.md: ResNetLayer's third type parameter, the one volume-preserving feedforward struct, init_bias! → init_bias, GSympNet.act → activation, and the upscaling_activation spelling.

Tests

  • test/layers/layer_pins.jl and test/architectures/architecture_pins.jl: 27 layer cases, 13 architecture cases and the encoders and decoders of two autoencoders, each in Float32 and Float64, against the outputs that 4c4a8320 gives for the same seeds and inputs. The cases cover dense, SymmetricMatrix, triangular, SkewSymMatrix and Stiefel leaves.
  • test/layers/layer_structure.jl and test/architectures/architecture_structure.jl: the merged methods, the type parameters, the two bugs, the dimension check, the MHA draw order and head lookup by name, and the ::Type{T} grep.
  • test/layers/multi_head_attention_allocation.jl: the forward pass allocates what the same head products allocate with constant keys, within 1024 B over 1000 passes, for one and two heads.
  • test/architectures/forward_inference.jl: @inferred on the forward pass of every exported architecture with a chain, in Float32 and Float64.
  • test/layers/positional_encoding.jl: the layer on a JLArray with scalar indexing disallowed.

The MultiHeadAttention draw order is unchanged, and no seeded pin moved. The one initialparameters draws head by head, and in each head the query, the key and the value projection, as before.

Out of the Bool-ladder item: MultiHeadAttention (Stiefel, add_connection) and ClassificationLayer (average) encode other choices and keep their Bool parameters, and the ANN Dense layer is outside this part.

The pinned outputs were recorded on arm64 and are compared at 8 ULPs of the larger magnitude. The Linux and Windows CI jobs are the check of that tolerance on other platforms: in the critic's probe, a one-ULP change of the input moves 34 of 80 pin cases by more than 8 ULPs, so the pins are sensitive and those jobs must be read.

Suite at the critic-passed code: full suite with Metal on Julia 1.13.2, pass (Metal 40/40, one known broken: the JET @test_broken of #325); full suite with Metal on Julia 1.12.7 (the floor), pass (Metal 40/40, one broken); doctests pass; the Documenter docs build gives 0 unresolved references on Julia 1.13.2.

Critic

Verdict: PASS (round verify 2, branch simplify/part-m5-3 at 179d093, judged 7ada06d..HEAD). Mutants: 12 CAUGHT, 0 SURVIVED, 0 UNKNOWN, 0 INVALID. Blocking defects: none.

clause verdict
the SympNet layers keep the parent's dimension check (round-1 reproducers 16/16 and 4/4 pass; probe 12/12; mutants G1–G4 CAUGHT) PASS
MHA finds each head by its name, keys built once (reproducers 2/2 and 2/2; probe 66/66 in Float32 and Float64; mutants N3m, N3t, H1, H2, H3 CAUGHT) PASS
check 4 pins and the LASympNet Chain signature (units pass; mutants N4, D1, D2 CAUGHT) PASS
Zygote gradients through MHA unchanged (12 identical hashes against origin/main) PASS
the forward pass of every architecture infers (forward_inference.jl) PASS
the 18 test files that the fix diff reaches, and quality: 734 pass, 1 broken (the existing @test_broken isempty(reports) # #325 at quality/jet.jl:96) PASS
CHANGELOG matches the code; KNOWN_ISSUES K20 and K21 evidence true PASS

Earlier blocking defects (round 1): the SympNet dimension guard deleted; the MHA forward pass reading heads by field position. Both are fixed, and their reproducers pass.

The two commits after the critic's PASS change text only: KNOWN_ISSUES.md (K20's kind and scope, K22 to K26), the headline of one CHANGELOG.md entry, and the header comment of test/layers/layer_structure.jl.

Known issues added

  • K20 — LagrangianNeuralNetwork stores its activation in the field act. Present on 4c4a8320.

  • K21 — a comment in src/layers/symplectic_attention.jl names a test file under test/parameters/, which does not exist. Present on 4c4a8320.

  • K22 — the SympNet forward methods check the rows of q only, so a p of the wrong size fails later, in the product or the unbroadcast +, with another error than the layer's own. Present on 4c4a8320. The critic read this from the code and did not run it; it suggested that such an input returns a value. The run for this entry shows that every case raises an error and none returns a value.

  • K23 — decoder(::NeuralNetwork{<:AutoEncoder}) gives the decoder's UnknownDecoder or UnknownSymplecticDecoder the encoder's n_encoder_blocks; the model and the parameters read the right count, so only the stored field is wrong. Present on 4c4a8320, kept so that the outputs stay unchanged.

  • K24 — the docstrings of LinearSymplecticTransformer and SymplecticTransformer say that n_sympnet = 1 gives one GradientLayerQ and one GradientLayerP; with init_upper = true the chain has two GradientLayerQ. Present on 4c4a8320.

  • K25 — the SymplecticAutoencoder docstring says that decoder_init_q = true makes the first layer of each decoder block a p-type layer; it is a GradientLayerQ. Present on 4c4a8320.

  • K26 — the UnknownDecoder docstring names its third argument n_encoder_blocks and links to itself for an example. Present on 4c4a8320.

Known issues closed: B11, C19, K5.

Reported, not fixed

  • decoder(::NeuralNetwork{<:AutoEncoder}) gives the decoder's UnknownDecoder the encoder's n_encoder_blocks (K23). It is kept so that the outputs stay unchanged.
  • VolumePreservingFeedForward (the architecture) keeps its if/else init_upper ladder.
  • LagrangianNeuralNetwork.act (K20).
  • The stale comment path in symplectic_attention.jl (K21).
  • The SympNet half-size check (K22).
  • The transformer docstrings on n_sympnet = 1 (K24).

Advisor decisions

For review, verbatim from the plan of this part:

Decided (advisor, 2026-10-19, high): check 4's last sentence reads: "encoder/decoder of a
SymplecticAutoencoder keep NonLinearSymplecticEncoder/NonLinearSymplecticDecoder, and those of
a PSDArch (SymplecticCompression) keep UnknownSymplecticEncoder/UnknownSymplecticDecoder, as
on the parent commit 4c4a8320." The branch's code stays as it is. The test asserts all three
families over REAL_ELTYPES: the SymplecticAutoencoder pair, the PSDArch pair, and a test-local
struct PlainAE <: AutoEncoder that gives UnknownEncoder/UnknownDecoder.

Decided (advisor, 2026-10-19, medium): check 3's "equals" is abs(measured - literal) ≤ 1024 B,
with k = 1000 forward passes inside one function-barrier call and literal > 0 as the control. One
rebuilt key costs at least 240 B per pass, so the mutant exceeds the tolerance by more than 230×.

(The plan corrects that figure to the measured one: one rebuilt key costs 192 B a pass on Julia 1.13.2, 187× the 1024 B tolerance. The tolerance stays as decided.)

Decided (advisor, 2026-10-20, high): check 2's "σ === tanh" reads "σ === Dense(4, 6, tanh).σ",
because AbstractNeuralNetworks' Dense stores Activation(tanh) = TanhActivation() (ANN
src/activation.jl:29, src/layers/dense.jl:5-8), so no tree can satisfy the literal. The test
also builds the architecture with the default upscaling_activation and transformer_dim = 6, and
asserts Chain(arch).layers[1].σ === Dense(4, 6, identity).σ. Tests catch: Chain calling
Dense(arch.sys_dim, arch.transformer_dim) without the activation picks Dense's default tanh,
and the default-activation assertion fails. A constructor that drops the keyword stores identity,
and the tanh assertion 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 initialparameters and the head
keys 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 initialparameters make the StandardTransformerIntegrator
pins (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 it
lists, and each other GML layer that encodes one of the same choices. In GML, only ResNetLayer
does, for the bias. "The same kind in every layer" means a Symbol type parameter in those layers.
ResNetLayer encodes its bias as :bias/:no_bias, as the VPFF layer does, and dispatches its
initialparameters, parameterlength and forward methods on that Symbol. Its use_bias::Bool
keyword stays. MultiHeadAttention (Stiefel, add_connection) and ClassificationLayer
(average) encode other choices, have no ladder, and keep their Bool parameters. ANN Dense
is 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 passes
use_bias through as a Bool fails both. In Float32 and Float64,
parameterlength(ResNetLayer(3; use_bias = false)) == 9 and == 12 with the bias.
ResNetLayer(3, tanh; use_bias = false) gives x + tanh.(W * x), with eltype === T. The
mutant that sends :no_bias to the bias method fails both.

(The advisor's test/architectures/forward_pins.jl is test/architectures/architecture_pins.jl on this branch.)

Pre-PR verification

Two passes. Pass 1 ran over the full branch. Pass 2 ran over the text-only commits after the critic's PASS, and it checked every CHANGELOG line that the branch adds.

Fixed

  • Pass 1: the CHANGELOG headline of the Symbol-parameter change said too much. It now names ResNetLayer's bias parameter, the single volume-preserving feedforward struct, and the two renamed fields and the renamed keyword.
  • Pass 1: the header comment of test/layers/layer_structure.jl now names the layers whose Bool keywords become Symbol parameters.
  • Pass 1: K23 and K24 recorded. Pass 2: K25 and K26 recorded. Each is the same on 4c4a8320.

Unresolved

  • nit: the testset name "every Bool keyword becomes a Symbol type parameter" (test/layers/layer_structure.jl) says too much: MultiHeadAttention's Stiefel and the architectures' init_upper keywords stay Bool. Left as is, because the test code does not change after the critic's PASS.

Checked and clean

  • Correctness: the full diff was read. No stale callers of the renamed or removed names in src, test, docs, scripts or ext. The four LASympNet combinations match the base. The CHANGELOG counts and the K20, K22–K25 evidence reproduce.
  • Type piracy: Aqua test_piracies passes.
  • Type stability: the changed forward methods infer concretely in Float32 and Float64 (82 combinations). initialparameters(::MultiHeadAttention) cannot infer on the base either, because its keys depend on the runtime n_heads.
  • Allocations: fatou lint is clean. The shared _shift methods and PositionalEncoding allocate the same bytes as the base bodies, with equal outputs, in a warm session. The 384 000 B difference for a two-head MultiHeadAttention over 1000 passes at the base reproduces (Julia 1.13.2).
  • Comments and history: every comment next to a hunk was read. No history in the added lines outside CHANGELOG.md.
  • The test layout check passes.

Not checked

  • Inference on the 1.12 floor in the verifier; the suite, with forward_inference.jl, passed on 1.12.7.
  • Fresh-process allocation figures in the verifier.
  • A GPU device run; PositionalEncoding on a device is covered by the JLArray testset.

🤖 Generated with Claude Code

michakraus and others added 5 commits October 20, 2026 22:17
…Bool choice and triangle

- _shift(Val(C), z, f) serves the Q/P pairs of the SympNet, symplectic attention and
  linear symplectic attention layers; every Bool keyword picks a Symbol type parameter
  in one expression, and ResNetLayer encodes its bias as :bias/:no_bias.
- VolumePreservingFeedForwardLayer is the one struct of the lower and upper layers,
  whose initialparameters takes init_bias.
- MultiHeadAttention has one initialparameters, keeps its draw order, and builds no
  head key per forward pass.
- LASympNet has one Chain; _gradient_layers serves GSympNet, LST and ST; the DataLoader
  constructors delegate; GSympNet.activation; one encoder and decoder per AutoEncoder.
- LinearSymplecticTransformer keeps its activation; upscaling_activation is spelled
  right; PositionalEncoding allocates on the backend of its input.
- Pins against 4c4a832 at 8 ULPs, structure tests and forward inference tests.
  Closes known issues B11, C19 and K5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The three merged SympNet forward methods check size(x.q, 1) == M ÷ 2 again,
as the parent does; a dimension-2 activation layer broadcast a half of any
length without it. The MHA forward pass finds each head by its name head_i
from a generated constant tuple, so a reordered parameter set gives the
parent's output and no key is built per pass. Tests for both, the LASympNet
Chain signature on one line, the out-of-scope comment path restored, and
known issues K20 and K21 recorded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
K20 is a late finding about a field name, and the architectures with
several activations name them with a prefix. K22 records the measured
behaviour of a p of the wrong size: an error from the product or the
unbroadcast +, never a value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…23, K24

The verifier found that the headline and the header of
test/layers/layer_structure.jl claimed every layer encodes its choices as
a Symbol, while MultiHeadAttention and ClassificationLayer keep Bool
parameters. K23 (decoder block count) and K24 (transformer docstrings)
are present on 4c4a832.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

📝 Summary

Summary by CodeRabbit

  • Breaking Changes

    • Renamed configuration fields and type parameters, including the upscaling activation and SympNet activation field.
    • Updated compatibility requirements and layer construction interfaces.
  • Bug Fixes

    • Custom activations now propagate correctly through architectures.
    • Positional encoding now works with device-backed arrays and preserves the input backend.
    • Corrected autoencoder wiring and initialization behavior.
  • Performance

    • Reduced repeated allocations and removed redundant zeroing work.
  • Tests

    • Added broad regression coverage for layers, architectures, inference, outputs, and backend-aware positional encoding.

Priority: ⬇️ Low

Change: Refactor

Merge Risk: 🔵 Low · up to c0300

The decoder wrapper can store the wrong block count when encoder and decoder block counts differ. Model outputs and parameters are not affected. The fix is a one-word change to the argument on the line the PR already edits.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly identifies the M5 refactor, including shared Q/P methods, Boolean-choice encoding, and triangle consolidation.
Description check Passed The description directly explains the refactor, behavior-preserving changes, breaking API changes, tests, known issues, and verification results.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (29 skipped: 29 unsupported.)

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 92ffaa16-27f6-49ca-9fd9-8942f33614e7
📥 Commits

Reviewing files that changed from the base of the PR and between 4c4a832 and c03002a.

📒 Files selected for processing: 29
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • src/architectures/autoencoder.jl
  • src/architectures/linear_symplectic_transformer.jl
  • src/architectures/standard_transformer_integrator.jl
  • src/architectures/symplectic_autoencoder.jl
  • src/architectures/symplectic_transformer.jl
  • src/architectures/sympnet.jl
  • src/layers/linear_symplectic_attention.jl
  • src/layers/multi_head_attention.jl
  • src/layers/positional_encoding.jl
  • src/layers/psd_like_layer.jl
  • src/layers/resnet.jl
  • src/layers/symplectic_attention.jl
  • src/layers/sympnets.jl
  • src/layers/transformer.jl
  • src/layers/volume_preserving_attention.jl
  • src/layers/volume_preserving_feedforward.jl
  • test/architectures/architecture_pins.jl
  • test/architectures/architecture_structure.jl
  • test/architectures/forward_inference.jl
  • test/helpers/pinned_outputs.jl
  • test/helpers/pins.jl
  • test/integration/documented_defaults.jl
  • test/layers/layer_pins.jl
  • test/layers/layer_structure.jl
  • test/layers/multi_head_attention_allocation.jl
  • test/layers/positional_encoding.jl
  • test/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.

NeuralNetwork(
UnknownDecoder(nn.architecture.full_dim, nn.architecture.reduced_dim, nn.architecture.n_encoder_blocks),
decoder_model(nn.architecture),
_unknown_decoder(arch)(arch.full_dim, arch.reduced_dim, arch.n_encoder_blocks),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pass arch.n_decoder_blocks to the decoder wrapper.

This PR rewrites Line 236. The third argument of UnknownDecoder and UnknownSymplecticDecoder fills the field n_decoder_blocks. The line passes arch.n_encoder_blocks to that field. If an AutoEncoder has different encoder and decoder block counts, the decoder architecture stores the wrong count. KNOWN_ISSUES.md K23 records this defect.

decoder_model(arch) and decoder_parameters(nn) already read arch.n_decoder_blocks. The fix therefore changes only the stored field, and no pinned output moves. The PR already edits this line, so the fix can go in now, and K23 can close.

🐛 Proposed fix
--- "a/src/architectures/autoencoder.jl"
+++ "b/src/architectures/autoencoder.jl"
@@ -233,7 +233,7 @@
 function decoder(nn::NeuralNetwork{<:AutoEncoder})
     arch = nn.architecture
     NeuralNetwork(
-        _unknown_decoder(arch)(arch.full_dim, arch.reduced_dim, arch.n_encoder_blocks),
+        _unknown_decoder(arch)(arch.full_dim, arch.reduced_dim, arch.n_decoder_blocks),
         decoder_model(arch),
         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.

Suggested change
_unknown_decoder(arch)(arch.full_dim, arch.reduced_dim, arch.n_encoder_blocks),
_unknown_decoder(arch)(arch.full_dim, arch.reduced_dim, arch.n_decoder_blocks),

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant