Skip to content

Keep zero convolutional and local kernels finite during normalization - #814

Merged
Hananel-Hazan merged 3 commits into
BindsNET:masterfrom
AHMETHAKANBEZIR1:fix/zero-kernel-normalization
Oct 23, 2026
Merged

Hananel-Hazan merged 3 commits into
BindsNET:masterfrom
AHMETHAKANBEZIR1:fix/zero-kernel-normalization

Conversation

@AHMETHAKANBEZIR1

@AHMETHAKANBEZIR1 AHMETHAKANBEZIR1 commented Oct 21, 2026 •

Copy link
Copy Markdown

Zero kernels in Conv1dConnection, Conv2dConnection, Conv3dConnection and the corresponding dimensional LocalConnection classes become NaN during normalization: the weight sum is zero, so the in-place multiplication evaluates 0 * (norm / 0). A zero normalization target also causes this on subsequent calls after it has zeroed nonzero weights.

Use denominator 1 only for genuinely all-zero kernel groups, keeping those weights zero. Each nonzero group's existing signed-sum normalization is retained; a nonzero signed kernel whose values cancel is not silently treated as an empty kernel. The six method docstrings and Unreleased changelog describe zero-kernel handling. No extra weight parameters or dependencies are introduced.

Forty-two cases cover all six classes, float32/float64 normalization, disabled/zero/positive targets, mixed zero and nonzero groups, repeated calls, parameter identity, float32 public pre-activation computations and nonzero signed-cancellation controls. Unchanged master fails 24 cases and passes 18 controls.

Validation

Windows CPU / Python 3.12.14 / torch 2.14.0+cpu / torchvision 0.29.0+cpu:

  • Final full pytest suite: 234 passed, 1 skipped, 8 warnings.
  • Sparse tensor invariant checks were enabled throughout the full run; the 2 GiB available-memory guard did not trigger.
  • Project-wide isort/Black checks through Poetry/pre-commit and Git diff checks pass.

GPU execution and a full documentation build were not run. CPU torch/torchvision and formatting tool versions follow the same verified project pins/profile reported in #813. No lockfile, dependency, workflow or sparse-check settings are changed. This PR targets master independently of #813.

Prepared and tested autonomously with OpenAI Codex. The account owner has not independently reviewed this patch before submission; no human-only authorship or review is claimed.

Local integration check: #813 and #814 apply together without conflicts, including both changelog entries. The full suite passes with 254 passed, 1 skipped, 8 warnings, sparse tensor invariant checks enabled, and the memory guard not triggered. Project-wide pre-commit checks also pass. The private integration branch was not pushed; each PR keeps its independent master base.

Co-authored-by: OpenAI Codex <codex@openai.com>
Review follow-up to BindsNET#814. One tensor operation replaces the per-kernel loop in
Conv1d/2d/3d and LocalConnection1D/2D/3D, and LocalConnection gets the same
guard. Kernels whose weights sum to zero keep their weights instead of
becoming NaN or infinite. CPU results match the old loop bit for bit; CUDA
float32 can differ by rounding for kernels of 81+ weights.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Hananel-Hazan

Copy link
Copy Markdown
Collaborator

Hi @AHMETHAKANBEZIR1,

Thanks for finding this. A kernel whose weights sum to zero turns into NaN on the next normalize(), and from there the NaN spreads through the rest of the run, so this one needs to be fixed.

The guard you added runs inside the existing Python loop over kernels, and it makes normalize() about twice as slow as master. Time per normalize() call (median of 7 runs):

Shape master this PR (61ab1dc) pushed commit (6720b51)
Conv2d, conv_mnist sizes, CPU 0.64 ms 1.34 ms 0.03 ms
LocalConnection1D, CPU 124.9 ms 265.2 ms 2.8 ms
LocalConnection1D, CUDA 338.5 ms 771.7 ms 0.09 ms

I took your fix and pushed 6720b51 on top of it:

  1. One tensor operation over all kernels replaces the loop.
  2. A kernel whose weights sum to zero is left unchanged. That covers all-zero kernels and also kernels like [1, -1, 0], which your second test expected to become +inf and -inf. An infinite weight breaks a run just like a NaN, so that test now checks the kernel stays finite and unchanged.
  3. LocalConnection had the same division by zero, now fixed too.
  4. A test in test/network/test_perf_equivalence.py pins the new code to the old loop. CPU results are identical bit for bit. On CUDA, float32 kernels of 81 or more weights can differ by rounding (about 1e-7), because the sum is taken in a different order.

The full suite passes (250 tests). Seeded training runs of all six connection classes plus LocallyConnectedNetwork end with exactly the same weights as master on CPU.

Can you run it on your side and confirm it works for you?

Resolve CHANGELOG.md: keep the zero-sum kernel entry at the end of Fixed and
master's new Added section after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Hananel-Hazan
Hananel-Hazan merged commit 78ec02b into BindsNET:master Oct 23, 2026
1 check passed
@AHMETHAKANBEZIR1

Copy link
Copy Markdown
Author

Confirmed on merged commit 78ec02b: the complete local suite passes (255 passed, 1 skipped; PyTorch 2.14.0+cpu). This includes the vectorized-vs-loop CPU equivalence checks, signed zero-sum kernels staying finite and unchanged, and the LocalConnection zero-column case. I did not run CUDA or reproduce the timing measurements.

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.

2 participants