Skip to content

[BUGFIX] Add activation output to residual when layer1x1 is inactive - #343

Open
andrenepomuceno wants to merge 1 commit into
sdatkinson:mainfrom
andrenepomuceno:fix/inactive-layer1x1-residual
Open

andrenepomuceno wants to merge 1 commit into
sdatkinson:mainfrom
andrenepomuceno:fix/inactive-layer1x1-residual

Conversation

@andrenepomuceno

Copy link
Copy Markdown

Fixes #341

Problem

When layer1x1 is inactive, detail::Layer::Process copied the layer input straight to _output_next_layer, so the residual path dropped the layer's activation. The trainer computes residual = x + layer_output, and layer_output is post_activation when there is no layer1x1 (nam/models/wavenet/_layer_array.py, Layer.forward, ~L755–773 in neural-amp-modeler 0.13). Any model exported with "layer1x1": {"active": false} therefore rendered differently from the trainer.

Change

  • NAM/wavenet/model.cpp: with layer1x1 inactive, _output_next_layer = input + _z.topRows(bottleneck). This is the activation output after the optional activation_post_film, the same tensor the trainer adds.
    • Eigen path: one expression.
    • NAM_USE_INLINE_GEMM path: the memcpy becomes a per-column add. It uses _z's real row stride because _z has 2 * bottleneck rows under gated/blended activations.
  • NAM/wavenet/detail.h: updated the doc comments that described the old identity residual.

No new validation is needed. detail::Layer already throws when layer1x1 is inactive and bottleneck != channels, and test_layer1x1_inactive_bottleneck_mismatch covers that. Neither the A2 fast path (it requires an active layer1x1) nor the slimmable WaveNet (it reuses detail::Layer) needs changes.

Tests

  • test_layer1x1_inactive: the expected value now comes from the trainer's formula (1 + ReLU(2) = 3). It used to be 1, which asserted the bug.
  • New test_layer1x1_inactive_gated: a gated layer with per-frame values and constant gates. It checks that only the gated (top) rows of _z reach the residual, which exercises the strided copy in the inline GEMM build.
  • New test_layer1x1_inactive_full_model: a two-layer, single-channel WaveNet loaded through get_dsp from a JSON config with layer1x1 inactive. The expected output is computed in the test from x1 = x + ReLU(a0*x + b0 + m0*x), so the second layer (and the output) depend on the residual.

Cross-check: a randomly initialised neural-amp-modeler 0.13 WaveNet with inactive layer1x1 and a bounded Hardtanh, rendered with tools/render, matches the PyTorch forward pass (pad_start=True, 4096 samples) to a max abs error of 7.5e-8 with this PR and #342 (Hardtanh bounds) applied, down from 1.9e-3 on main.

run_tests passes in both the default and -DNAM_USE_INLINE_GEMM builds (g++ 11, Linux). Before the fix, the updated test_layer1x1_inactive fails in both builds.

Note

This changes the output of existing .nam files that have layer1x1 inactive: they now match the trainer, but the difference can be audible for models that were only ever heard through the core.

With layer1x1 inactive, the trainer computes residual = x + post_activation,
but the layer passed the input through unchanged. Add the activation output
(top bottleneck rows of _z) to the input in both the Eigen and inline GEMM
paths.

Fixes sdatkinson#341
dfernandes83 added a commit to dfernandes83/NeuralAmpModelerCore that referenced this pull request Oct 6, 2026
…s() larger than max buffer), sdatkinson#343 (keep activation in the residual when layer1x1 is inactive), sdatkinson#344 (honor Hardtanh min_val/max_val)
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.

[BUG] Inactive layer1x1 drops the activation from the WaveNet residual path

1 participant