Skip to content

feat(nn): implement GroupNormalization forward pass (#1381) - #1463

Merged
michalharakal merged 2 commits into
developfrom
feature/1379-groupnorm-forward
Oct 31, 2026
Merged

michalharakal merged 2 commits into
developfrom
feature/1379-groupnorm-forward

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Summary

Implements Lane 2 (kotlin-core) sub-issue #1381 of the GroupNormalization feature #1379.

GroupNormalization was reachable from the public groupNorm {} DSL builder but crashed on the first forward pass: its four forward helpers were TODO(), eps was never applied, and gamma/beta were never broadcast. This PR implements the forward pass with torch.nn.GroupNorm semantics using ops that already exist.

Part of #1379.
Closes #1381.

Changes

  1. Forward pass (sk.ainet.lang.nn.normalization.GroupNormalization):

    • (N, C, *) is reshaped to (N, G, C/G * prod(*)); each group is normalized with its own biased mean/variance (matching PyTorch), eps enters the denominator as (x - mean) / sqrt(var + eps), and the result is reshaped back.
    • The numerically-sensitive normalization runs in f32 regardless of model dtype, mirroring LayerNormalization in the same package.
    • Per-channel gamma/beta are reshaped to (1, C, 1, ...) for broadcasting over batch and spatial dimensions. The reshape runs on the input's ops: the default placeholder params carry VoidTensorOps, whose shape-only reshape returns zeros and would silently wipe the affine parameters (the same latent hazard exists in BatchNormalization, which reshapes stats through data-bearing tensors only).
    • Input rank and channel count are validated with require().
    • @NotImplemented removed — the layer leaves the ❌ list in the coverage matrix.
  2. Tests (skainet-backend-cpu/src/commonTest/.../GroupNormalizationTest.kt, 10 tests):

    • Outputs compared element-wise against a double-precision reference implementation of torch.nn.GroupNorm (per-group biased stats + per-channel affine), within 1e-5.
    • Coverage: 2D (N, C), 3D and 4D inputs; per-channel gamma/beta broadcast (distinct values per channel, the bug-prone axis); default placeholder affine (= the groupNorm {} DSL path) equals non-affine output; G = 1 (layer-norm-like) and G = C (instance-norm-like) extremes; constant input stays finite via eps; custom eps enters the denominator; wrong channel count and indivisible groups throw IllegalArgumentException.

Checks run locally

  • :skainet-lang:skainet-lang-core:jvmTest and :skainet-backends:skainet-backend-cpu:jvmTest — green
  • :skainet-lang:skainet-lang-core:jvmApiCheck — no public API shape change
  • checkUnmarkedStubs — green (no allowlist entry needed; all TODOs removed)

Note

Lane 1 (#1380, PyTorch reference values) has no posted fixtures yet; the tests encode the reference formula directly in double precision, which is bit-equivalent to torch.nn.GroupNorm for these fixture sizes. Happy to swap in posted fixtures when #1380 lands. Docs (#1382) and review (#1383) remain as separate lanes.

Implements the four TODO() forward helpers with torch.nn.GroupNorm
semantics: (N, C, *) is reshaped to (N, G, C/G * prod(*)), each group
is normalized with its own biased mean/variance, eps enters the
denominator, and the result is reshaped back.

- Numerically-sensitive normalization runs in f32 regardless of model
  dtype, mirroring LayerNormalization.
- Per-channel gamma/beta are reshaped to (1, C, 1, ...) through the
  input's ops for broadcasting; the default placeholder params carry
  VoidTensorOps, whose shape-only reshape would zero the values.
- Input rank and channel count are validated with require().
- @NotImplemented removed; the layer leaves the coverage-matrix gap list.

Tests in skainet-backend-cpu commonTest compare against a double-
precision reference implementation of torch.nn.GroupNorm covering
2D/3D/4D inputs, per-channel affine, single group, groups == channels
(instance norm), constant input, custom eps and validation failures.

Part of #1379.
@github-actions

Copy link
Copy Markdown

📖 Documentation Preview

The documentation has been built successfully for this PR.

Generated Files:

  • Operator documentation: docs/modules/operators/_generated_/
  • JSON schema output: operators.json

Artifacts:

  • Download the documentation-preview-1463 artifact to view the complete documentation locally.

This comment will be updated automatically when the PR is updated.

GroupNormalization moves from the NN-layers gap list to done (31/32).
Regenerated with the HLO and backward coverage dumps per the
documentation block in build.gradle.kts.
@github-actions

Copy link
Copy Markdown

📖 Documentation Preview

The documentation has been built successfully for this PR.

Generated Files:

  • Operator documentation: docs/modules/operators/_generated_/
  • JSON schema output: operators.json

Artifacts:

  • Download the documentation-preview-1463 artifact to view the complete documentation locally.

This comment will be updated automatically when the PR is updated.

@michalharakal
michalharakal merged commit deb5f5b into develop Oct 31, 2026
28 of 32 checks passed
@michalharakal
michalharakal deleted the feature/1379-groupnorm-forward branch October 31, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Lane 2 · kotlin-core] GroupNormalization — implement GroupNormalization.forward

2 participants