Skip to content

CI: close test-guard bypasses found by independent review - #832

Merged
Hananel-Hazan merged 17 commits into
masterfrom
ci-guard-hardening
Oct 29, 2026
Merged

Hananel-Hazan merged 17 commits into
masterfrom
ci-guard-hardening

Conversation

@Hananel-Hazan

Copy link
Copy Markdown
Collaborator

What this changes and why

An independent review of the test guards from #828 and #830 found ways a pull request could pass every required check while breaking results. The worst case: one commit halved the leak of LIFNodes. It also added a root pytest.ini that skips test/regression, and a fake "bug test" that only checks which folder it runs from. It passed build, bug-test, weakened-tests and the coverage floor. This PR closes the holes. I replayed each of the reviewer's attacks against the new scripts to check.

This branch is stacked on #831, because both change test/regression/op_counts.json. Merge #831 first; this PR's diff then shrinks to the CI changes.

Changes

  • CI ignores other config files. build and the 3.11/3.12 jobs run pytest -c pyproject.toml --cov-config=pyproject.toml, so a pytest.ini, setup.cfg, tox.ini or .coveragerc cannot change what runs. Replayed: with the planted pytest.ini, CI's command still runs the goldens, and 16 of 17 fail.
  • weakened-tests (.github/scripts/weakened_tests.py):
    • It runs pytest --collect-only on the base and the head, and flags every test id that disappears. That covers del test_x, __test__ = False, a shortened parameter list, and a conftest that deselects tests.
    • It uses --no-renames, so a renamed test file is no longer invisible.
    • New source checks:
      • tolerances passed by position (allclose, isclose, approx);
      • a tolerance keyword newly added;
      • annotated tolerance constants, and constants built from arithmetic;
      • widened helper defaults;
      • fewer exact comparisons;
      • checks that cannot fail (assert ... or True, except AssertionError, an early return);
      • skips counted per function and at module level, instead of per file.
    • It flags every change to pyproject.toml, pytest.ini, .pytest.ini, tox.ini, setup.cfg, .coveragerc, test/regression/networks.py and test/regression/cpu_pins.py, and torch version changes in poetry.lock.
    • Replayed: all 13 weakening techniques from the review are flagged.
  • bug-test (.github/scripts/fails_before_fix.py):
    • Only a test that ran on the base and failed there counts. A test that cannot be imported on the base, because it imports a name the PR adds, is listed as "not counted".
    • The summary shows each counted test's failure message on the base, so a maintainer can see a fake test. This cannot be fully automated, and the docs say so.
    • --base and --head are resolved to commit ids first, so the documented local command, --base master --head HEAD, now gives the same answer as CI.
  • Torch upgrades. The golden and op-count tests now fail on CI (CI set) instead of skipping when torch's version differs from the stored files. So a torch upgrade must regenerate them in the same PR.
  • CPU pins. networks.deterministic() also checks that MKL_CBWR and ATEN_CPU_CAPABILITY hold the pinned values.
  • New golden network conn_postpre_mean. It runs PostPre with reduction=torch.mean at batch 4 and dt 0.5, which exercises the general update path. A dt factor added there passed every test before; now test_golden[conn_postpre_mean] fails.
  • Operation counts now include one forward() of every Nodes class; before, only LIFNodes was counted. Check: an extra copy in IFNodes.forward fails both IFNodes workloads.
  • bench fails on large slowdowns. It now fails when a workload's median time ratio is above 2× the base, with the lower end of the 95% interval above 1.5, or when the tool errors. That catches slow pure-Python code, which the operation counts cannot see. The no-change calibration never exceeded 1.12 in any round. It also now takes its base as HEAD^1.
  • Coverage floors for every package, each one point under CI run 40804291700. Before, only network and learning had floors.
  • CODEOWNERS now covers all of test/, pyproject.toml, poetry.lock and the pytest and coverage config files.

Settings to change after merge (repository settings, not code)

  • Add bench to the required checks.
  • Turn on "Dismiss stale pull request approvals" and "Require approval of the most recent reviewable push". Otherwise an approval, or a tests-changed-ok label, keeps applying after a contributor pushes new commits.

Checklist

  • pytest -c pyproject.toml: 641 passed locally; coverage floors pass.
  • Changed recorded files: op_counts.json (54 workloads, with a reason) and a new golden conn_postpre_mean. This needs tests-changed-ok.
  • AI tool: Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

Hananel-Hazan and others added 16 commits October 28, 2026 22:19
…SRMNodes reset clears its spike window

IzhikevichNodes sized its parameters by the n argument (None when only shape is
given) and never reshaped them; CSRMNodes contracted with 'i,kij->kj'. Both now
work for any shape; one-dimensional results are bit-identical (goldens
conv2d_if_izh and csrm_window trial 0 unchanged). CSRMNodes.reset_state_variables
now zeroes last_spikes, which changes results after a reset (csrm_window
golden regenerated with that reason).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…with one channel

LocalConnection.compute dropped the batch dimension in its final view.
MaxPool{1,2,3}d multiplied a missing decay (None) and squeeze()d away a channel
of size 1; a missing decay now means plain spike counts, and the firing-rate
buffer follows the spike tensor's shape. Results that ran before are unchanged
(goldens pass). Reproducers kept as test_fixed_bug_stays_fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; NetworkMonitor reset; tuple target shapes

- MCC_LearningRule bounded sparse weights into a new tensor that only the rule
  held, so DiehlAndCook2015(sparse=True) never learned; the bound and the
  sparse normalisation now write in place. At batch 1 sparse and dense learn
  bit-identical weights without norm (test_sparse_dc2015_learns_like_dense);
  with norm they differ by summation order (max 6.0e-8).
- MulticompartmentConnection.compute applies per-sample sparse weight copies
  ([batch, src, tgt]) with bmm; one copy keeps the old arithmetic.
- NetworkMonitor.reset_state_variables looked connections up in layers.
- [batch] + target.shape failed for tuple shapes.

Results change: DiehlAndCook2015(sparse=True) now learns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…work with batch > 1

- LearningRule.reset_state_variables deletes the tensors a rule creates on its
  first update (_step_state); AbstractConnection.reset_state_variables forwards
  to the rule. Reset then equals a freshly built network (invariant tests for
  mstdp_mstdpet and rules_mix now pass). Results change after a reset with
  learning on: goldens mstdp_mstdpet(_dt05), rules_mix(_dt05) regenerated,
  trial 0 bit-identical.
- Rmax and MCC_learning.MSTDPET keep per-sample state and reduce the update
  over the batch, as MSTDP does. Batch 1 is bit-identical to the old formula
  (TestMccMstdpetBatched, goldens); batch 2 with sum equals the sum of single
  samples to summation order. Op counts at batch 1: MCC MSTDPET 24 -> 27,
  Rmax 24 -> 26 (regenerated with reason). API snapshot: +1 method.

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

Adopted from #813 (head 53dd2e3) by AHMETHAKANBEZIR1, reviewed with the
bindsnet-pr-review skill (sandboxed canaries, fail-then-pass, API, timing);
Hananel adopted it as accountable author because the PR says no human
reviewed it. MeanFieldConnection.normalize scales w in place by norm over the
absolute weight sum (a shared scalar counted once per target), instead of
assigning a plain tensor to the Parameter (TypeError on every call).

Co-Authored-By: Hananel Hazan <hananel@hazan.org.il>
Co-Authored-By: OpenAI Codex <codex@openai.com>
Adopted from #815 (head 49c57f8) by AHMETHAKANBEZIR1, reviewed with the
bindsnet-pr-review skill; Hananel adopted it as accountable author because the
PR says no human reviewed it. Connection.compute_window re-creates s_w when the
batch size changes, and reset_state_variables clears it for CSRM targets.

Co-Authored-By: Hananel Hazan <hananel@hazan.org.il>
Co-Authored-By: OpenAI Codex <codex@openai.com>
meanfield_norm and csrm_window goldens regenerated (both used to record a
crash). Invariant xfails removed: reset-equals-fresh for csrm_window and
meanfield_norm, round trip and batch for meanfield_norm. Still open: the CSRM
spike window is not in the state_dict (round trip).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ction drives CSRMNodes

- Connection.s_w is a registered buffer, so state_dict saves it; a load adopts
  a saved window or drops one the saved state lacked (_load_window). The
  csrm_window round-trip invariant now passes; golden regenerated (adds s_w,
  no value changed).
- MulticompartmentConnection.compute_window read s_w and self.w, neither of
  which it has, so MCC into CSRMNodes always raised. It now keeps a window of
  its own outputs (pipeline runs once per step), resized on a batch change,
  cleared on reset, saved in the state_dict. With static weights it matches a
  Connection bit for bit (test_multicompartment_into_csrm_matches_connection).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_decay

- compute() took the mean over the whole batch, so one sample's input depended
  on the others; it now uses each sample's own mean (batch 1 bit-identical:
  goldens pass without the weight_decay change).
- weight_decay was passed by position into AbstractConnection's reduction
  slot and ignored; it is now passed by name. Results change for anyone who
  set weight_decay on a MeanFieldConnection (golden meanfield_decay
  regenerated with that reason).
- Two #813 test lines expected the batch-wide mean (0.75); corrected to the
  per-sample means.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on features at batch > 1, sparse weight bounds

- topology_features.MeanField never called the base constructor (no name) and
  had no initial value; it now has an optional name, a placeholder value, and
  a per-sample mean ([batch, src, tgt], applied per sample by compute).
- Sub-features (Normalization, Updating) get no-op prime_feature, update,
  normalize and reset_state_variables, which MulticompartmentConnection calls
  on every pipeline entry; both now work in the constructor and in Network.run.
- AdaptationBaseSynapsHistory/OtherSynaps: a synapse is active when its
  pre-synaptic neuron spiked in any sample (the mask is shared connectivity);
  batch 1 gives the same values.
- Sparse weights with wmin/wmax: Connection.__init__ bounds a dense copy,
  LearningRule.update bounds in place via a dense copy, and
  WeightDependentPostPre/DiehlAndCook compute their factors from a dense view.
  PostPre, WeightDependentPostPre and DiehlAndCook on a bounded
  SparseConnection learn bit-identical weights to the dense Connection.
- Test workarounds for these bugs removed (batch-1-only sets, broken pairs,
  tuple-shape and sparse-bound special cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mpartmentConnection feature values in the state_dict

- MulticompartmentConnection saves its pipeline feature values as
  feature.<name> and loads them in place, so DiehlAndCook2015 can be saved
  and restored (the last four strict xfails in test_invariants.py now pass;
  dc2015 goldens regenerated: new keys, no value changed).
- Every KNOWN_BUGS entry is fixed: the dict becomes FIXED_BUGS (reproducers
  that must keep running); the empty xfail machinery in test_invariants.py and
  the batch-1, no-learning-check and sub-feature workarounds in
  test_all_classes.py are removed, so those classes run at batch 2 with full
  checks.
- CONTRIBUTING, PR template and bindsnet/learning/README.md updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Network (reviewer B):
- MulticompartmentConnection state_dict: refuses to save duplicate feature
  names (they shared one key, and loading put one feature's value into the
  other); checks shape and sparse layout on load (copy_ broadcast a wrong-shaped
  tensor silently); saves the adaptation features' mask, spike buffer and
  counter (only value was saved).
- An Updating sub-feature drives its parent's learning; the connection's
  update skips that parent (it learned twice per step).
- A sub-feature whose parent is not in the pipeline is refused at construction
  (it failed later with an unclear TypeError). insert_pipeline had list.insert's
  arguments swapped.
- Connection.compute_window and MaxPool resets create state on the spikes'
  device (CUDA device mismatch, also on master).
- Docstrings state how the two CSRM windows differ once weights learn.

Learning rules (reviewer A):
- MCC_learning.DiehlAndCook crashed on sparse weights (twin of the learning.py
  fix). Sparse MCC weight copies now all get the batch's shared update in
  PostPre, DiehlAndCook and MSTDPET (as MSTDP did), so sparse equals dense; the
  default squeeze reduction means a sum over a larger batch, as in the fused
  dense path (_reduce_batch, _add_to_feature).
- Docs: per-sample reward shape [batch, 1, 1]; reset keeps the reduction chosen
  at construction.

CHANGELOG: checkpoint compatibility (strict=False for old checkpoints), new
state shapes, sparse and Updating semantics. Each fix has a test that fails on
ce98f93 and passes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…), docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Hananel-Hazan Hananel-Hazan added the tests-changed-ok Maintainer reviewed every item the weakened-tests check listed label Oct 29, 2026
master's tree equals 02219bf, the #831 head this branch was built on, so the
branch's files are kept unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Hananel-Hazan
Hananel-Hazan merged commit 9908bb4 into master Oct 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests-changed-ok Maintainer reviewed every item the weakened-tests check listed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants