Skip to content

CBraMod: concrete head whenever the geometry is known, so from_pretrained works (stacked on #1226) - #1233

Merged
bruAristimunha merged 5 commits into
braindecode:masterfrom
bruAristimunha:fix/cbramod-lazy-head
Oct 6, 2026
Merged

bruAristimunha merged 5 commits into
braindecode:masterfrom
bruAristimunha:fix/cbramod-lazy-head

Conversation

@bruAristimunha

Copy link
Copy Markdown
Collaborator

Summary

CBraMod.from_pretrained("braindecode/cbramod-pretrained", chs_info=…, n_times=…, sfreq=…) fails at every geometry with Attempted to use an uninitialized parameter … LazyModule (audit in #1227, 10/10 cells).

Root cause: CBraMod sizes its head from the private _n_chans / _n_times, which stay None when the geometry is given as chs_info / input_window_seconds; the model then builds a LazyLinear head although n_chans and n_times are perfectly known, and a LazyLinear cannot be loaded (or saved) before a forward pass.

Stacked on #1226 (review the last commit). Part of #1227.

Changes

  • cbramod.py: _knows_geometry() (the public properties resolve) decides between Linear and LazyLinear; head and _n_patch() use self.n_chans / self.n_times. Lazy fallback kept for a truly unknown geometry.
  • test_foundation_models.py: geometry from chs_info + input_window_seconds → concrete head, save_pretrained → from_pretrained round trip equal, lazy head only when geometry is unknown.
  • docs/whats_new.rst (Bug fixes).

Testing

  • pytest test/unit_tests/models/test_foundation_models.py test/unit_tests/models/test_integration.py -k cbramod → 14 passed, 2 skipped.

  • pre-commit run --files <changed> clean. Not run: full suite (CI); the Hub checkpoint itself (cluster audit will be re-run after the stack lands).

  • Regression test

  • Style checks recorded

  • docs/whats_new.rst updated

…od, LUNA; ZUNA default on_non_divisible='pad'
Copilot AI balanced review requested due to automatic review settings October 5, 2026 13:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 3 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Fixes CBraMod’s from_pretrained failure by ensuring the classification head is concrete (Linear) whenever channel/time geometry is known (including when derived from chs_info / input_window_seconds), while standardizing non-divisible window handling via PatchTokenizer across models.

Changes:

  • CBraMod: detect “known geometry” via public properties and build Linear head accordingly; keep LazyLinear only for truly unknown geometry.
  • Labram/LUNA/ZUNA: route time-axis tokenization through PatchTokenizer with new on_non_divisible behavior (ZUNA default now "pad").
  • Tests + docs: add regression coverage for padding/default behavior and for CBraMod save/load round-trip; document behavior change and bug fix.
File Description
test/​unit_tests/​models/​test_foundation_models.py Updates/extends tests for default padding behavior and CBraMod save/load regression.
docs/​whats_new.rst Documents non-divisible window behavior changes and CBraMod LazyLinear head bug fix.
braindecode/​models/​zuna.py Changes on_non_divisible default to "pad" and updates docstring accordingly.
braindecode/​models/​luna.py Introduces PatchTokenizer for time tokenization and adds on_non_divisible parameter.
braindecode/​models/​labram.py Adds on_non_divisible support and centralizes pad/crop/error handling via PatchTokenizer.
braindecode/​models/​cbramod.py Uses PatchTokenizer + “known geometry” logic to avoid non-serializable lazy heads when geometry is derivable.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +284 to +286
x_signal = self.tokenizer._prepare_input(x_signal)
if mask is not None:
mask = self.tokenizer._prepare_input(mask)
Comment on lines +847 to +854
self, embed_dim: int = 64, patch_size: int = 40, on_non_divisible: str = "pad"
) -> None:
super().__init__()
self.patch_size = patch_size
self.embed_dim = embed_dim
self.tokenizer = PatchTokenizer(
patch_size=patch_size, n_times=patch_size, on_non_divisible=on_non_divisible
)
Comment on lines +2004 to +2016
@pytest.mark.parametrize("cls,kwargs,patch_size", _patch_models())
def test_non_divisible_window_padded_by_default(cls, kwargs, patch_size):
"""A window that is not a multiple of patch_size pads (warning) and only
raises with on_non_divisible="error"; the tokenizer adds no parameters."""
kwargs = dict(kwargs, n_times=kwargs["n_times"] + 37, n_outputs=2)
with pytest.warns(UserWarning, match="not divisible"):
model = cls(**kwargs).eval()
n_chans = kwargs.get("n_chans") or len(kwargs["chs_info"])
x = torch.randn(1, n_chans, kwargs["n_times"])
with torch.no_grad():
out = model(x, ch_names=_TEN_TWENTY) if cls is Labram else model(x)
assert torch.isfinite(out).all()
assert not any("tokenizer" in k for k in model.state_dict())
Comment on lines +190 to 196
# Shared tokenizer: (batch, n_chans, n_times) -> (batch, n_chans, n_patch, patch_size),
# padding/cropping a non-divisible time axis at forward time.
self.rearrange = PatchTokenizer(
patch_size=patch_size,
n_times=self.n_times if self._knows_geometry() else patch_size,
on_non_divisible=on_non_divisible,
)
Copilot AI balanced review requested due to automatic review settings October 5, 2026 13:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment on lines +190 to 196
# Shared tokenizer: (batch, n_chans, n_times) -> (batch, n_chans, n_patch, patch_size),
# padding/cropping a non-divisible time axis at forward time.
self.rearrange = PatchTokenizer(
patch_size=patch_size,
n_times=self.n_times if self._knows_geometry() else patch_size,
on_non_divisible=on_non_divisible,
)
Comment on lines +1035 to +1037
self.tokenizer = PatchTokenizer(
patch_size=patch_size, n_times=n_times, on_non_divisible=on_non_divisible
)
X_patch: Tensor
[batch, n_chans, n_times//patch_size, patch_size]
"""
x = self.tokenizer._prepare_input(x)
Comment on lines +284 to +286
x_signal = self.tokenizer._prepare_input(x_signal)
if mask is not None:
mask = self.tokenizer._prepare_input(mask)
Comment thread docs/whats_new.rst
:class:`braindecode.modules.PatchTokenizer` and accept a window that is not a
multiple of ``patch_size`` by right zero-padding the last patch (with a warning),
through a new ``on_non_divisible={"pad", "crop", "error"}`` argument; previously
such windows raised a reshape error. :class:`braindecode.models.ZUNA` now defaults to
Conflict resolution:
- braindecode/models/cbramod.py: kept master's braindecode#1226 PatchTokenizer on_non_divisible padding and merged the PR's _knows_geometry() helper (superset of master's _n_times/_n_chans-not-None check) into the PatchTokenizer n_times arg, final_layer construction (both __init__ sites), reset_head, and _n_patch.
- docs/whats_new.rst: kept both the braindecode#1233 CBraMod lazy-head bug-fix entry and master's braindecode#1207/braindecode#1212 Deep4Net/ShallowFBCSPNet bug-fix entries.
- test/unit_tests/models/test_foundation_models.py: kept master's MAPA/PopT test blocks and the PR's test_cbramod_head_is_concrete_when_geometry_is_derived.
Copilot AI balanced review requested due to automatic review settings October 5, 2026 15:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 1 High severity · 6 Medium severity · 7 Low severity

Open (14)

Comment thread braindecode/models/cbramod.py Outdated
self.rearrange = PatchTokenizer(
patch_size=patch_size,
n_times=self._n_times if self._n_times is not None else patch_size,
n_times=self.n_times if self._knows_geometry() else patch_size,
if return_encoder_output:
self.final_layer = nn.Identity()
elif self._n_times is not None and self._n_chans is not None:
elif self._knows_geometry():
# A head implies a classifier, also when built with return_encoder_output.
self._update_init_kwargs(return_encoder_output=False)
if self._n_times is not None and self._n_chans is not None:
if self._knows_geometry():
head can be a real Linear instead of a LazyLinear that cannot be saved
or loaded before a forward pass."""
try:
self.n_chans, self.n_times
model.save_pretrained(tmp_path)
loaded = CBraMod.from_pretrained(tmp_path)
x = torch.randn(1, len(chs), 800)
assert torch.allclose(model.eval()(x), loaded.eval()(x), atol=1e-5)
@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.41%. Comparing base (02f6b55) to head (1b2207d).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1233      +/-   ##
==========================================
+ Coverage   88.40%   88.41%   +0.01%     
==========================================
  Files         156      156              
  Lines       18999    19019      +20     
==========================================
+ Hits        16796    16816      +20     
  Misses       2203     2203              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI balanced review requested due to automatic review settings October 5, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 1 High severity · 6 Medium severity · 9 Low severity

Open (16)

head can be a real Linear instead of a LazyLinear that cannot be saved
or loaded before a forward pass."""
try:
self.n_chans, self.n_times
model.save_pretrained(tmp_path)
loaded = CBraMod.from_pretrained(tmp_path)
x = torch.randn(1, len(chs), 800)
assert torch.allclose(model.eval()(x), loaded.eval()(x), atol=1e-5)
Resolved docs/whats_new.rst by keeping both entries.
Copilot AI balanced review requested due to automatic review settings October 6, 2026 06:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The implementation consistently uses resolved geometry and includes focused regression coverage for serialization and fallback behavior.

Review effort: Balanced
Findings: 1 High severity · 6 Medium severity · 9 Low severity

Open (16)

@bruAristimunha
bruAristimunha merged commit 28b51ff into braindecode:master Oct 6, 2026
21 of 22 checks passed
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