bidirectional-mamba 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 redEddie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+
5
+ recursive-include bidirectional_mamba *.py
6
+
7
+ prune tests
8
+ prune benchmarks
9
+ prune .git
10
+ prune .pytest_cache
11
+ prune dist
12
+ prune build
13
+ prune *.egg-info
14
+
15
+ global-exclude *.pyc
16
+ global-exclude __pycache__
17
+ global-exclude .DS_Store
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: bidirectional-mamba
3
+ Version: 0.2.0
4
+ Summary: Bidirectional Mamba-2 SSD block with SwiGLU FFN (Vim/VMamba-style merge).
5
+ Author: redEddie
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/redEddie/bidirectional-mamba
8
+ Keywords: mamba,ssm,bidirectional,vision-mamba,swiglu
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: torch>=2.1
15
+ Requires-Dist: einops>=0.4
16
+ Requires-Dist: mamba-ssm>=2.0
17
+ Requires-Dist: causal-conv1d>=1.2
18
+ Requires-Dist: matplotlib>=3.0
19
+ Dynamic: license-file
20
+
21
+ # bidirectional-mamba
22
+
23
+ 양방향 Mamba-2 SSD 블록에 SwiGLU FFN을 결합한 Vision-Mamba/VMamba 스타일 구현체입니다.
24
+ forward scan과 backward scan을 독립적으로 수행하고, 출력을 더한 뒤 gate와 out projection을 공유합니다.
25
+
26
+ ```
27
+ zxbcdt = in_proj(u) # [z | x | B | C | dt | dt_b], 하나의 projection
28
+ y_fw = SSD(conv (x), A, D, dt, dt_bias )
29
+ y_bw = SSD(conv_b(x_rev), A_b, D_b, dt_b_rev, dt_bias_b)_rev # reversed
30
+ y = y_fw + y_bw # sum merge
31
+ out = out_proj(RMSNormGated(y, z))
32
+ ```
33
+
34
+ 블록 구조는 `pre-norm` 기반이며 `(mixer residual) + (SwiGLU FFN residual)`입니다.
35
+ 스택 마지막에는 final RMSNorm이 붙습니다.
36
+
37
+ ## 설치
38
+
39
+ CUDA + Triton 환경이 필요합니다 (`mamba-ssm`, `causal-conv1d` 필요). CPU-only는 지원하지 않습니다.
40
+
41
+ ```bash
42
+ pip install torch einops matplotlib
43
+ pip install mamba-ssm causal-conv1d --no-build-isolation
44
+ pip install -e .
45
+ ```
46
+
47
+ ## 기본 사용법
48
+
49
+ ```python
50
+ import torch
51
+ from bidirectional_mamba import BiMambaStack
52
+
53
+ stack = BiMambaStack(d_model=768, n_layers=4, d_state=64, headdim=64,
54
+ chunk_size=49).cuda()
55
+
56
+ x = torch.randn(2, 196, 768, device="cuda") # [B, L, D]
57
+ y = stack(x) # [B, L, D]
58
+ ```
59
+
60
+ ## 주요 기능
61
+
62
+ ### 1. 방향별 분리 (`share_dt`, `share_bc`)
63
+
64
+ 기본적으로 forward/backward는 각자의 `A`, `D`, `dt_bias`, `conv1d`를 가집니다.
65
+ 추가로 `dt` 투영과 `B/C` 투영도 방향별로 분리할 수 있습니다.
66
+
67
+ ```python
68
+ # dt 분리: backward scan이 자신만의 dt를 가짐
69
+ BiMambaStack(d_model=768, n_layers=4, share_dt=False)
70
+
71
+ # B/C 분리: backward scan이 자신만의 B, C를 가짐
72
+ BiMambaStack(d_model=768, n_layers=4, share_dt=False, share_bc=False)
73
+ ```
74
+
75
+ ### 2. Padding mask (`valid_mask`)
76
+
77
+ 가변 길이 배치를 다룰 때 반드시 `valid_mask`를 전달하세요.
78
+ padding이 없으면 backward scan이 padding 영역에서 시작해 실제 토큰까지 영향을 미칩니다.
79
+
80
+ ```python
81
+ mask = torch.zeros(B, L, dtype=torch.bool, device="cuda")
82
+ mask[i, :lengths[i]] = True
83
+
84
+ y = stack(x, valid_mask=mask)
85
+ ```
86
+
87
+ mask를 쓰면 padding 위치는 0을 출력하고 실제 토큰에는 영향을 주지 않습니다.
88
+
89
+ ### 3. 학습 건강 지표 (`return_stats`)
90
+
91
+ `BiMambaStack`, `BiMambaBlock`, `BiMambaMixer` 모두 `return_stats=True`를 지원합니다.
92
+ loss 외에도 dt, A decay, B/C norm, forward/backward 기여도 등 Mamba 고유의 수치를 확인할 수 있습니다.
93
+
94
+ ```python
95
+ out, stats = stack(x, return_stats=True)
96
+
97
+ # stats 예시
98
+ # {
99
+ # "layer0/mixer/dt/effective_mean": ...,
100
+ # "layer0/mixer/A/decay_mean": ...,
101
+ # "layer0/mixer/y_fw_bw_ratio": ...,
102
+ # ...
103
+ # }
104
+ ```
105
+
106
+ 자세한 예제는 `benchmarks/log_mamba_stats.py`를 참고하세요.
107
+
108
+ ### 5. 구현 검증
109
+
110
+ `benchmarks/verify_bidirectional.py`는 양방향 Mamba 구현이 제대로 되었는지 확인하는 독립 스크립트입니다.
111
+ flip equivariance, backward branch 사용 여부, explicit reference match, gradient flow, valid mask 등 8가지 기준을 검사합니다.
112
+
113
+ ```bash
114
+ python benchmarks/verify_bidirectional.py
115
+ ```
116
+
117
+ ### 4. SSD token-mixing 행렬 (`mixing_matrix`)
118
+
119
+ Mamba의 암묵적인 token-mixing 행렬 L을 attention map처럼 시각화할 수 있습니다.
120
+ forward scan은 하삼각, backward scan은 상삼각을 채웁니다.
121
+
122
+ ```python
123
+ mixer = BiMambaMixer(d_model=192, d_state=32, headdim=32).cuda()
124
+ L = mixer.mixing_matrix(x) # [B, L, L]
125
+ ```
126
+
127
+ 시각화 예제는 `benchmarks/visualize_mixing_matrix.py`를 참고하세요.
128
+
129
+ ## CIFAR-10 벤치마크 결과
130
+
131
+ **공통 설정**: patch_size=4, share_dt=False, share_bc=True, use_mlp=True, epochs=30, seed=0
132
+
133
+ ### Width scaling (n_layers=8 고정)
134
+
135
+ | d_model | n_layers | headdim | params(추정) | test acc |
136
+ |--------:|---------:|--------:|------------:|---------:|
137
+ | 768 | 8 | 64 | ~67M | 80.70% |
138
+ | 1024 | 8 | 128 | ~119M | 81.76% |
139
+ | 4096 | 4 | 128 | ~944M | 80.13% |
140
+
141
+ ### Depth scaling (d_model=384 고정, LR per-depth scaling)
142
+
143
+ | d_model | n_layers | headdim | params(추정) | test acc |
144
+ |--------:|---------:|--------:|------------:|---------:|
145
+ | 384 | 4 | 32 | ~8.5M | 84.39% |
146
+ | 384 | 8 | 32 | ~17M | 85.00% |
147
+ | 384 | 12 | 32 | ~25M | 85.04% |
148
+ | 384 | 16 | 32 | ~34M | 85.23% |
149
+ | 384 | 20 | 32 | ~42M | 84.65% |
150
+
151
+ > d_model=384이 768/1024 width scaling보다 훨씬 적은 파라미터로 더 높은 성능을 냈습니다.
152
+ > 이는 현재 width scaling 설정(특히 4096)이 CIFAR-10에 비해 과도하게 크다는 것을 의미합니다.
153
+
154
+ ## 공개 API
155
+
156
+ - `BiMambaMixer` — 양방향 SSD mixer (shared in/out projection, 방향별 conv + A + D + dt + dt_bias).
157
+ - `BiMambaBlock` — pre-norm block: `mixer + SwiGLU FFN` residual.
158
+ - `BiMambaStack` — `n`개 block + final RMSNorm.
159
+ - `SwiGLUMLP` — 독립형 SwiGLU FFN (expand=8/3 기본).
160
+
161
+ 세 클래스 모두 `share_dt`, `share_bc`를 받고, `forward`는 `valid_mask`를 받습니다.
162
+ `BiMambaMixer/Block/Stack`은 추가로 `return_stats=True`를 받을 수 있습니다.
163
+
164
+ ## 참고
165
+
166
+ - `A`/`A_b`, `D`/`D_b`, `dt_bias`/`dt_bias_b`, 두 `conv1d`, 그리고 (`share_dt=False`일 때) 두 `dt` 투영은 모두 묶여 있지 않습니다.
167
+ - `in_proj`, `RMSNormGated` (z로 gate), `out_proj`는 공유됩니다.
168
+ 출력을 더한 뒤 gate를 적용하는 것은 `norm_before_gate=False`이므로 방향별로 따로 gate하는 것과 동일합니다.
169
+ - `share_dt=True`는 이전 동작으로 돌아가 forward `dt`를 reversed해서 backward가 재사용합니다.
170
+ softplus가 단조증가이고 per-head bias만 분리되므로 두 방향이 "어떤 토큰이 중요한지"에 대해 강제로 동의해야 합니다.
171
+ - autoregressive 디코딩용 `step()`은 없습니다. 양방향 scan은 전체 시퀀스가 필요합니다.
172
+ - `seq_idx` 기반 세그먼트 경계는 아직 구현되지 않았습니다.
173
+
174
+ ## 참고 문헌
175
+
176
+ - **Vision Mamba (Vim)** — Zhu et al., *Vision Mamba: Efficient Visual Representation Learning with Bidirectional State Space Model*, 2024. [arXiv:2401.09417](https://arxiv.org/abs/2401.09417)
177
+ - **VMamba** — Liu et al., *VMamba: Visual State Space Model*, 2024. [arXiv:2401.10166](https://arxiv.org/abs/2401.10166)
178
+ - **SwiGLU FFN** — Shazeer, *GLU Variants Improve Transformer*, 2020. [arXiv:2002.05202](https://arxiv.org/abs/2002.05202)
179
+ - **Mamba-2 / SSD** — Dao & Gu, *Transformers are SSMs: Generalized Models and Efficient Algorithms Through Structured State Space Duality*, 2024. [arXiv:2405.21060](https://arxiv.org/abs/2405.21060)
@@ -0,0 +1,159 @@
1
+ # bidirectional-mamba
2
+
3
+ 양방향 Mamba-2 SSD 블록에 SwiGLU FFN을 결합한 Vision-Mamba/VMamba 스타일 구현체입니다.
4
+ forward scan과 backward scan을 독립적으로 수행하고, 출력을 더한 뒤 gate와 out projection을 공유합니다.
5
+
6
+ ```
7
+ zxbcdt = in_proj(u) # [z | x | B | C | dt | dt_b], 하나의 projection
8
+ y_fw = SSD(conv (x), A, D, dt, dt_bias )
9
+ y_bw = SSD(conv_b(x_rev), A_b, D_b, dt_b_rev, dt_bias_b)_rev # reversed
10
+ y = y_fw + y_bw # sum merge
11
+ out = out_proj(RMSNormGated(y, z))
12
+ ```
13
+
14
+ 블록 구조는 `pre-norm` 기반이며 `(mixer residual) + (SwiGLU FFN residual)`입니다.
15
+ 스택 마지막에는 final RMSNorm이 붙습니다.
16
+
17
+ ## 설치
18
+
19
+ CUDA + Triton 환경이 필요합니다 (`mamba-ssm`, `causal-conv1d` 필요). CPU-only는 지원하지 않습니다.
20
+
21
+ ```bash
22
+ pip install torch einops matplotlib
23
+ pip install mamba-ssm causal-conv1d --no-build-isolation
24
+ pip install -e .
25
+ ```
26
+
27
+ ## 기본 사용법
28
+
29
+ ```python
30
+ import torch
31
+ from bidirectional_mamba import BiMambaStack
32
+
33
+ stack = BiMambaStack(d_model=768, n_layers=4, d_state=64, headdim=64,
34
+ chunk_size=49).cuda()
35
+
36
+ x = torch.randn(2, 196, 768, device="cuda") # [B, L, D]
37
+ y = stack(x) # [B, L, D]
38
+ ```
39
+
40
+ ## 주요 기능
41
+
42
+ ### 1. 방향별 분리 (`share_dt`, `share_bc`)
43
+
44
+ 기본적으로 forward/backward는 각자의 `A`, `D`, `dt_bias`, `conv1d`를 가집니다.
45
+ 추가로 `dt` 투영과 `B/C` 투영도 방향별로 분리할 수 있습니다.
46
+
47
+ ```python
48
+ # dt 분리: backward scan이 자신만의 dt를 가짐
49
+ BiMambaStack(d_model=768, n_layers=4, share_dt=False)
50
+
51
+ # B/C 분리: backward scan이 자신만의 B, C를 가짐
52
+ BiMambaStack(d_model=768, n_layers=4, share_dt=False, share_bc=False)
53
+ ```
54
+
55
+ ### 2. Padding mask (`valid_mask`)
56
+
57
+ 가변 길이 배치를 다룰 때 반드시 `valid_mask`를 전달하세요.
58
+ padding이 없으면 backward scan이 padding 영역에서 시작해 실제 토큰까지 영향을 미칩니다.
59
+
60
+ ```python
61
+ mask = torch.zeros(B, L, dtype=torch.bool, device="cuda")
62
+ mask[i, :lengths[i]] = True
63
+
64
+ y = stack(x, valid_mask=mask)
65
+ ```
66
+
67
+ mask를 쓰면 padding 위치는 0을 출력하고 실제 토큰에는 영향을 주지 않습니다.
68
+
69
+ ### 3. 학습 건강 지표 (`return_stats`)
70
+
71
+ `BiMambaStack`, `BiMambaBlock`, `BiMambaMixer` 모두 `return_stats=True`를 지원합니다.
72
+ loss 외에도 dt, A decay, B/C norm, forward/backward 기여도 등 Mamba 고유의 수치를 확인할 수 있습니다.
73
+
74
+ ```python
75
+ out, stats = stack(x, return_stats=True)
76
+
77
+ # stats 예시
78
+ # {
79
+ # "layer0/mixer/dt/effective_mean": ...,
80
+ # "layer0/mixer/A/decay_mean": ...,
81
+ # "layer0/mixer/y_fw_bw_ratio": ...,
82
+ # ...
83
+ # }
84
+ ```
85
+
86
+ 자세한 예제는 `benchmarks/log_mamba_stats.py`를 참고하세요.
87
+
88
+ ### 5. 구현 검증
89
+
90
+ `benchmarks/verify_bidirectional.py`는 양방향 Mamba 구현이 제대로 되었는지 확인하는 독립 스크립트입니다.
91
+ flip equivariance, backward branch 사용 여부, explicit reference match, gradient flow, valid mask 등 8가지 기준을 검사합니다.
92
+
93
+ ```bash
94
+ python benchmarks/verify_bidirectional.py
95
+ ```
96
+
97
+ ### 4. SSD token-mixing 행렬 (`mixing_matrix`)
98
+
99
+ Mamba의 암묵적인 token-mixing 행렬 L을 attention map처럼 시각화할 수 있습니다.
100
+ forward scan은 하삼각, backward scan은 상삼각을 채웁니다.
101
+
102
+ ```python
103
+ mixer = BiMambaMixer(d_model=192, d_state=32, headdim=32).cuda()
104
+ L = mixer.mixing_matrix(x) # [B, L, L]
105
+ ```
106
+
107
+ 시각화 예제는 `benchmarks/visualize_mixing_matrix.py`를 참고하세요.
108
+
109
+ ## CIFAR-10 벤치마크 결과
110
+
111
+ **공통 설정**: patch_size=4, share_dt=False, share_bc=True, use_mlp=True, epochs=30, seed=0
112
+
113
+ ### Width scaling (n_layers=8 고정)
114
+
115
+ | d_model | n_layers | headdim | params(추정) | test acc |
116
+ |--------:|---------:|--------:|------------:|---------:|
117
+ | 768 | 8 | 64 | ~67M | 80.70% |
118
+ | 1024 | 8 | 128 | ~119M | 81.76% |
119
+ | 4096 | 4 | 128 | ~944M | 80.13% |
120
+
121
+ ### Depth scaling (d_model=384 고정, LR per-depth scaling)
122
+
123
+ | d_model | n_layers | headdim | params(추정) | test acc |
124
+ |--------:|---------:|--------:|------------:|---------:|
125
+ | 384 | 4 | 32 | ~8.5M | 84.39% |
126
+ | 384 | 8 | 32 | ~17M | 85.00% |
127
+ | 384 | 12 | 32 | ~25M | 85.04% |
128
+ | 384 | 16 | 32 | ~34M | 85.23% |
129
+ | 384 | 20 | 32 | ~42M | 84.65% |
130
+
131
+ > d_model=384이 768/1024 width scaling보다 훨씬 적은 파라미터로 더 높은 성능을 냈습니다.
132
+ > 이는 현재 width scaling 설정(특히 4096)이 CIFAR-10에 비해 과도하게 크다는 것을 의미합니다.
133
+
134
+ ## 공개 API
135
+
136
+ - `BiMambaMixer` — 양방향 SSD mixer (shared in/out projection, 방향별 conv + A + D + dt + dt_bias).
137
+ - `BiMambaBlock` — pre-norm block: `mixer + SwiGLU FFN` residual.
138
+ - `BiMambaStack` — `n`개 block + final RMSNorm.
139
+ - `SwiGLUMLP` — 독립형 SwiGLU FFN (expand=8/3 기본).
140
+
141
+ 세 클래스 모두 `share_dt`, `share_bc`를 받고, `forward`는 `valid_mask`를 받습니다.
142
+ `BiMambaMixer/Block/Stack`은 추가로 `return_stats=True`를 받을 수 있습니다.
143
+
144
+ ## 참고
145
+
146
+ - `A`/`A_b`, `D`/`D_b`, `dt_bias`/`dt_bias_b`, 두 `conv1d`, 그리고 (`share_dt=False`일 때) 두 `dt` 투영은 모두 묶여 있지 않습니다.
147
+ - `in_proj`, `RMSNormGated` (z로 gate), `out_proj`는 공유됩니다.
148
+ 출력을 더한 뒤 gate를 적용하는 것은 `norm_before_gate=False`이므로 방향별로 따로 gate하는 것과 동일합니다.
149
+ - `share_dt=True`는 이전 동작으로 돌아가 forward `dt`를 reversed해서 backward가 재사용합니다.
150
+ softplus가 단조증가이고 per-head bias만 분리되므로 두 방향이 "어떤 토큰이 중요한지"에 대해 강제로 동의해야 합니다.
151
+ - autoregressive 디코딩용 `step()`은 없습니다. 양방향 scan은 전체 시퀀스가 필요합니다.
152
+ - `seq_idx` 기반 세그먼트 경계는 아직 구현되지 않았습니다.
153
+
154
+ ## 참고 문헌
155
+
156
+ - **Vision Mamba (Vim)** — Zhu et al., *Vision Mamba: Efficient Visual Representation Learning with Bidirectional State Space Model*, 2024. [arXiv:2401.09417](https://arxiv.org/abs/2401.09417)
157
+ - **VMamba** — Liu et al., *VMamba: Visual State Space Model*, 2024. [arXiv:2401.10166](https://arxiv.org/abs/2401.10166)
158
+ - **SwiGLU FFN** — Shazeer, *GLU Variants Improve Transformer*, 2020. [arXiv:2002.05202](https://arxiv.org/abs/2002.05202)
159
+ - **Mamba-2 / SSD** — Dao & Gu, *Transformers are SSMs: Generalized Models and Efficient Algorithms Through Structured State Space Duality*, 2024. [arXiv:2405.21060](https://arxiv.org/abs/2405.21060)
@@ -0,0 +1,20 @@
1
+ from ._mlp import SwiGLUMLP
2
+
3
+ __version__ = "0.2.0"
4
+ __all__ = ["SwiGLUMLP"]
5
+
6
+ # Mamba-2 SSD stack (requires causal_conv1d + mamba_ssm with SSD kernels)
7
+ try:
8
+ from .block import BiMambaBlock
9
+ from .mixer import BiMambaMixer
10
+ from .stack import BiMambaStack
11
+ __all__.extend(["BiMambaMixer", "BiMambaBlock", "BiMambaStack"])
12
+ except Exception:
13
+ BiMambaMixer = BiMambaBlock = BiMambaStack = None # type: ignore
14
+
15
+ # Mamba-3 bidirectional mixer (requires latest state-spaces/mamba source)
16
+ try:
17
+ from .mixer3 import BiMamba3Mixer
18
+ __all__.append("BiMamba3Mixer")
19
+ except Exception:
20
+ BiMamba3Mixer = None # type: ignore
@@ -0,0 +1,16 @@
1
+ import torch.nn as nn
2
+ import torch.nn.functional as F
3
+
4
+
5
+ class SwiGLUMLP(nn.Module):
6
+ def __init__(self, d_model: int, expand: float = 8 / 3, device=None, dtype=None):
7
+ factory_kwargs = {"device": device, "dtype": dtype}
8
+ super().__init__()
9
+ d_hidden = int(d_model * expand)
10
+ d_hidden = (d_hidden + 7) // 8 * 8
11
+ self.w1 = nn.Linear(d_model, d_hidden, bias=False, **factory_kwargs)
12
+ self.w2 = nn.Linear(d_model, d_hidden, bias=False, **factory_kwargs)
13
+ self.w3 = nn.Linear(d_hidden, d_model, bias=False, **factory_kwargs)
14
+
15
+ def forward(self, x):
16
+ return self.w3(F.silu(self.w1(x)) * self.w2(x))
@@ -0,0 +1,48 @@
1
+ import torch.nn as nn
2
+
3
+ try:
4
+ from mamba_ssm.ops.triton.layer_norm import RMSNorm
5
+ except ImportError:
6
+ RMSNorm = nn.LayerNorm
7
+
8
+ from ._mlp import SwiGLUMLP
9
+ from .mixer import BiMambaMixer
10
+
11
+ _NORM_EPSILON = 1e-5
12
+
13
+
14
+ class BiMambaBlock(nn.Module):
15
+ """Pre-norm bidirectional Mamba-2 block: mixer residual + SwiGLU FFN residual.
16
+
17
+ x = x + BiMambaMixer(RMSNorm(x))
18
+ x = x + SwiGLU (RMSNorm(x))
19
+ """
20
+
21
+ def __init__(self, d_model, d_state=64, headdim=64, expand=2,
22
+ chunk_size=49, dt_min=0.001, dt_max=0.02,
23
+ mlp_expand: float = 8 / 3, share_dt=False, share_bc=True,
24
+ device=None, dtype=None):
25
+ super().__init__()
26
+ factory_kwargs = {"device": device, "dtype": dtype}
27
+ self.norm1 = RMSNorm(d_model, eps=_NORM_EPSILON, **factory_kwargs)
28
+ self.mixer = BiMambaMixer(
29
+ d_model, d_state=d_state, headdim=headdim, expand=expand,
30
+ chunk_size=chunk_size, dt_min=dt_min, dt_max=dt_max,
31
+ share_dt=share_dt, share_bc=share_bc, **factory_kwargs,
32
+ )
33
+ self.norm2 = RMSNorm(d_model, eps=_NORM_EPSILON, **factory_kwargs)
34
+ self.mlp = SwiGLUMLP(d_model, expand=mlp_expand, **factory_kwargs)
35
+
36
+ def forward(self, x, valid_mask=None, return_stats=False):
37
+ mixer_in = self.norm1(x)
38
+ if return_stats:
39
+ mixer_out, mixer_stats = self.mixer(
40
+ mixer_in, valid_mask=valid_mask, return_stats=True)
41
+ else:
42
+ mixer_out = self.mixer(mixer_in, valid_mask=valid_mask)
43
+ mixer_stats = None
44
+ x = x + mixer_out
45
+ x = x + self.mlp(self.norm2(x))
46
+ if return_stats:
47
+ return x, {"mixer": mixer_stats}
48
+ return x
@@ -0,0 +1,384 @@
1
+ import math
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from einops import rearrange
7
+
8
+ from causal_conv1d import causal_conv1d_fn
9
+ from mamba_ssm.ops.triton.layernorm_gated import RMSNorm as RMSNormGated
10
+ from mamba_ssm.ops.triton.ssd_combined import mamba_chunk_scan_combined
11
+
12
+
13
+ class BiMambaMixer(nn.Module):
14
+ """Bidirectional Mamba-2 SSD mixer with shared in/out projection.
15
+
16
+ zxbcdt = in_proj(u) # [z | x | B | C | dt]
17
+ zxbcdt = in_proj(u) # [z | x | B | C | dt | dt_b]
18
+ y_fw = SSD(conv (x), A, D, dt, dt_bias )
19
+ y_bw = SSD(conv_b(x_rev), A_b, D_b, dt_b_rev, dt_bias_b)_rev
20
+ y = y_fw + y_bw # sum merge (Vim/VMamba)
21
+ out = out_proj(RMSNormGated(y, z))
22
+
23
+ Pass `valid_mask` whenever the batch is padded. Right-padding is harmless
24
+ for a causal model, but the backward scan starts inside the padding and
25
+ carries it into every real position, so an unmasked padded batch gives a
26
+ different answer for the same sequence depending on how much it was padded.
27
+ """
28
+
29
+ def __init__(self, d_model, d_state=64, headdim=64, expand=2, d_conv=4,
30
+ ngroups=1, chunk_size=49, dt_min=0.001, dt_max=0.02,
31
+ dt_init_floor=1e-4, A_init_range=(1, 16), share_dt=False,
32
+ share_bc=True, device=None, dtype=None):
33
+ super().__init__()
34
+ factory_kwargs = {"device": device, "dtype": dtype}
35
+ self.d_state = d_state
36
+ self.headdim = headdim
37
+ self.ngroups = ngroups
38
+ self.chunk_size = chunk_size
39
+ self.share_dt = share_dt
40
+ self.share_bc = share_bc
41
+ d_inner = expand * d_model
42
+ self.d_ssm = d_inner
43
+ assert d_inner % headdim == 0
44
+ self.nheads = d_inner // headdim
45
+
46
+ # dt is the only token-dependent quantity the two directions would
47
+ # otherwise share outright: x, B and C at least pass through a
48
+ # per-direction conv, but dt does not, so with share_dt the backward
49
+ # scan gets the forward dt reversed and can only shift it by a
50
+ # per-head bias -- softplus is monotonic, so the two directions are
51
+ # forced to agree on which tokens matter. A second projection costs
52
+ # d_model * nheads (0.5% of the mixer at d_model=768).
53
+ self._dt_splits = 1 if share_dt else 2
54
+ # B/C projections can also be separated per direction. When share_bc
55
+ # is True we keep the v0.2.0 layout [z | xBC | dt ...] for checkpoint
56
+ # compatibility; when False we emit [z | x | BC | BC_b | dt ...].
57
+ self._bc_splits = 1 if share_bc else 2
58
+ d_in_proj = (2 * d_inner + self._bc_splits * 2 * ngroups * d_state
59
+ + self._dt_splits * self.nheads)
60
+ self.in_proj = nn.Linear(d_model, d_in_proj, bias=False, **factory_kwargs)
61
+
62
+ conv_dim = d_inner + 2 * ngroups * d_state
63
+ self.conv1d = self._make_conv(conv_dim, d_conv, factory_kwargs)
64
+ self.conv1d_b = self._make_conv(conv_dim, d_conv, factory_kwargs)
65
+
66
+ self.dt_bias = self._init_dt_bias(dt_min, dt_max, dt_init_floor, factory_kwargs)
67
+ self.dt_bias_b = self._init_dt_bias(dt_min, dt_max, dt_init_floor, factory_kwargs)
68
+ self.A_log = self._init_A_log(A_init_range, device, dtype)
69
+ self.A_log_b = self._init_A_log(A_init_range, device, dtype)
70
+ self.D = self._init_D(device)
71
+ self.D_b = self._init_D(device)
72
+
73
+ self.norm = RMSNormGated(d_inner, eps=1e-5, norm_before_gate=False,
74
+ group_size=d_inner // ngroups, **factory_kwargs)
75
+ self.out_proj = nn.Linear(d_inner, d_model, bias=False, **factory_kwargs)
76
+
77
+ @staticmethod
78
+ def _make_conv(conv_dim, d_conv, factory_kwargs):
79
+ return nn.Conv1d(conv_dim, conv_dim, bias=True, kernel_size=d_conv,
80
+ groups=conv_dim, padding=d_conv - 1, **factory_kwargs)
81
+
82
+ def _init_dt_bias(self, dt_min, dt_max, dt_init_floor, factory_kwargs):
83
+ dt = torch.exp(
84
+ torch.rand(self.nheads, **factory_kwargs) * (math.log(dt_max) - math.log(dt_min))
85
+ + math.log(dt_min)
86
+ ).clamp(min=dt_init_floor)
87
+ inv_dt = dt + torch.log(-torch.expm1(-dt))
88
+ p = nn.Parameter(inv_dt)
89
+ p._no_weight_decay = True
90
+ return p
91
+
92
+ def _init_A_log(self, A_init_range, device, dtype):
93
+ A = torch.empty(self.nheads, dtype=torch.float32, device=device).uniform_(*A_init_range)
94
+ p = nn.Parameter(torch.log(A).to(dtype=dtype))
95
+ p._no_weight_decay = True
96
+ return p
97
+
98
+ def _init_D(self, device):
99
+ p = nn.Parameter(torch.ones(self.nheads, device=device))
100
+ p._no_weight_decay = True
101
+ return p
102
+
103
+ def _scan_dir(self, xBC, dt, A, conv, D, dt_bias, flip, mask=None,
104
+ return_intermediates=False):
105
+ if flip:
106
+ xBC, dt = xBC.flip(1), dt.flip(1)
107
+ if mask is not None:
108
+ mask = mask.flip(1)
109
+ dt_softplus = True
110
+ dt_input = dt
111
+ if mask is not None:
112
+ # Make masked steps inert in the recurrence. dt = 0 gives both
113
+ # decay = exp(0 * A) = 1, so the state passes through untouched,
114
+ # and dt * B * x = 0, so nothing is written. Zeroing xBC keeps the
115
+ # conv window from picking the masked region up, which leaves a
116
+ # position at the boundary seeing the same zeros causal_conv1d_fn
117
+ # already left-pads with at a true sequence start.
118
+ # softplus has to move out of the kernel to land on exactly zero.
119
+ xBC = xBC * mask
120
+ dt = F.softplus(dt.float() + dt_bias.float()).to(dt.dtype) * mask
121
+ dt_effective = dt
122
+ dt_bias, dt_softplus = None, False
123
+ else:
124
+ # The kernel applies softplus + bias internally. For diagnostics
125
+ # we materialise the effective step size without touching the
126
+ # tensor fed to the kernel.
127
+ dt_effective = F.softplus(dt.float() + dt_bias.float()).to(dt.dtype)
128
+ xBC = causal_conv1d_fn(
129
+ xBC.transpose(1, 2).contiguous(),
130
+ rearrange(conv.weight, "d 1 w -> d w"), bias=conv.bias, activation="silu",
131
+ ).transpose(1, 2)
132
+ x, B, C = torch.split(
133
+ xBC, [self.d_ssm, self.ngroups * self.d_state, self.ngroups * self.d_state], dim=-1)
134
+ y = mamba_chunk_scan_combined(
135
+ rearrange(x, "b l (h p) -> b l h p", p=self.headdim),
136
+ dt, A,
137
+ rearrange(B, "b l (g n) -> b l g n", g=self.ngroups),
138
+ rearrange(C, "b l (g n) -> b l g n", g=self.ngroups),
139
+ chunk_size=self.chunk_size, D=D, z=None, dt_bias=dt_bias,
140
+ dt_softplus=dt_softplus,
141
+ )
142
+ y = y.flip(1) if flip else y
143
+ if not return_intermediates:
144
+ return y
145
+ return y, {
146
+ "dt_raw": dt_input,
147
+ "dt_effective": dt_effective,
148
+ "x": x,
149
+ "B": B,
150
+ "C": C,
151
+ "xBC": xBC,
152
+ }
153
+
154
+ @staticmethod
155
+ def _broadcast_mask(mask, t):
156
+ """Broadcast `mask` to the same shape as `t` and return a bool tensor."""
157
+ if mask is None:
158
+ return None
159
+ while mask.dim() < t.dim():
160
+ mask = mask.unsqueeze(-1)
161
+ return mask.expand(t.shape).to(torch.bool)
162
+
163
+ @staticmethod
164
+ def _masked_stats(t, mask=None, dim=None):
165
+ """Return mean/std/min/max of `t` over unmasked positions.
166
+
167
+ `mask`: bool or float tensor broadcastable to `t`, True/non-zero on
168
+ real positions. If `mask` is None, all positions are used.
169
+ """
170
+ if mask is not None:
171
+ t = t[BiMambaMixer._broadcast_mask(mask, t)]
172
+ if t.numel() == 0:
173
+ nan = torch.tensor(float("nan"), device=t.device, dtype=t.dtype)
174
+ return nan, nan, nan, nan
175
+ mean = t.mean()
176
+ std = t.std(unbiased=False)
177
+ mn = t.min()
178
+ mx = t.max()
179
+ return mean.detach(), std.detach(), mn.detach(), mx.detach()
180
+
181
+ @staticmethod
182
+ def _masked_norm_mean(t, mask=None):
183
+ if mask is not None:
184
+ t = t[BiMambaMixer._broadcast_mask(mask, t)]
185
+ if t.numel() == 0:
186
+ return torch.tensor(float("nan"), device=t.device, dtype=t.dtype)
187
+ return t.norm(dim=-1).mean().detach()
188
+
189
+ def _compute_stats(self, z, A_decay, A_b_decay, fw, bw, mask=None):
190
+ """Build a detached statistics dict from scan intermediates.
191
+
192
+ `fw` and `bw` are the tuples returned by `_scan_dir` with
193
+ return_intermediates=True: (y, dict).
194
+ """
195
+ y_fw, fw_i = fw
196
+ y_bw, bw_i = bw
197
+
198
+ dt_raw_mean, dt_raw_std, dt_raw_min, dt_raw_max = self._masked_stats(
199
+ fw_i["dt_raw"], mask=mask)
200
+ dt_eff_mean, dt_eff_std, dt_eff_min, dt_eff_max = self._masked_stats(
201
+ fw_i["dt_effective"], mask=mask)
202
+ dt_b_raw_mean, dt_b_raw_std, _, _ = self._masked_stats(
203
+ bw_i["dt_raw"], mask=mask)
204
+ dt_b_eff_mean, dt_b_eff_std, dt_b_eff_min, dt_b_eff_max = self._masked_stats(
205
+ bw_i["dt_effective"], mask=mask)
206
+
207
+ A_mean, A_std, A_min, A_max = self._masked_stats(A_decay)
208
+ A_b_mean, A_b_std, _, _ = self._masked_stats(A_b_decay)
209
+
210
+ dt_bias_mean, dt_bias_std, _, _ = self._masked_stats(self.dt_bias)
211
+ dt_bias_b_mean, dt_bias_b_std, _, _ = self._masked_stats(self.dt_bias_b)
212
+
213
+ z_mean, z_std, _, _ = self._masked_stats(z, mask=mask)
214
+ y_fw_norm_mean = self._masked_norm_mean(y_fw, mask=mask)
215
+ y_bw_norm_mean = self._masked_norm_mean(y_bw, mask=mask)
216
+
217
+ stats = {
218
+ "dt/raw_mean": dt_raw_mean,
219
+ "dt/raw_std": dt_raw_std,
220
+ "dt/raw_min": dt_raw_min,
221
+ "dt/raw_max": dt_raw_max,
222
+ "dt/effective_mean": dt_eff_mean,
223
+ "dt/effective_std": dt_eff_std,
224
+ "dt/effective_min": dt_eff_min,
225
+ "dt/effective_max": dt_eff_max,
226
+ "dt_b/raw_mean": dt_b_raw_mean,
227
+ "dt_b/raw_std": dt_b_raw_std,
228
+ "dt_b/effective_mean": dt_b_eff_mean,
229
+ "dt_b/effective_std": dt_b_eff_std,
230
+ "dt_b/effective_min": dt_b_eff_min,
231
+ "dt_b/effective_max": dt_b_eff_max,
232
+ "A/decay_mean": A_mean,
233
+ "A/decay_std": A_std,
234
+ "A/decay_min": A_min,
235
+ "A/decay_max": A_max,
236
+ "A_b/decay_mean": A_b_mean,
237
+ "A_b/decay_std": A_b_std,
238
+ "dt_bias/mean": dt_bias_mean,
239
+ "dt_bias/std": dt_bias_std,
240
+ "dt_bias_b/mean": dt_bias_b_mean,
241
+ "dt_bias_b/std": dt_bias_b_std,
242
+ "x/norm_mean": self._masked_norm_mean(fw_i["x"], mask=mask),
243
+ "x_b/norm_mean": self._masked_norm_mean(bw_i["x"], mask=mask),
244
+ "B/norm_mean": self._masked_norm_mean(fw_i["B"], mask=mask),
245
+ "B_b/norm_mean": self._masked_norm_mean(bw_i["B"], mask=mask),
246
+ "C/norm_mean": self._masked_norm_mean(fw_i["C"], mask=mask),
247
+ "C_b/norm_mean": self._masked_norm_mean(bw_i["C"], mask=mask),
248
+ "conv_out/norm_mean": self._masked_norm_mean(fw_i["xBC"], mask=mask),
249
+ "conv_out_b/norm_mean": self._masked_norm_mean(bw_i["xBC"], mask=mask),
250
+ "z/mean": z_mean,
251
+ "z/std": z_std,
252
+ "y_fw/norm_mean": y_fw_norm_mean,
253
+ "y_bw/norm_mean": y_bw_norm_mean,
254
+ }
255
+ ratio = (y_fw_norm_mean / (y_bw_norm_mean + 1e-12)).item()
256
+ stats["y_fw_bw_ratio"] = torch.tensor(ratio, device=y_fw.device)
257
+ return stats
258
+
259
+ def _split_inputs(self, u):
260
+ """Split in_proj output into (z, xBC, xBC_b, dt, dt_b)."""
261
+ if self.share_bc:
262
+ sizes = ([self.d_ssm, self.d_ssm + 2 * self.ngroups * self.d_state]
263
+ + [self.nheads] * self._dt_splits)
264
+ z, xBC, dt, *rest = torch.split(self.in_proj(u), sizes, dim=-1)
265
+ xBC_b = xBC
266
+ dt_b = dt if self.share_dt else rest[0]
267
+ else:
268
+ sizes = ([self.d_ssm, self.d_ssm]
269
+ + [2 * self.ngroups * self.d_state] * self._bc_splits
270
+ + [self.nheads] * self._dt_splits)
271
+ z, x, BC, BC_b, dt, *rest = torch.split(self.in_proj(u), sizes, dim=-1)
272
+ dt_b = dt if self.share_dt else rest[0]
273
+ xBC = torch.cat([x, BC], dim=-1)
274
+ xBC_b = torch.cat([x, BC_b], dim=-1)
275
+ return z, xBC, xBC_b, dt, dt_b
276
+
277
+ def _mixing_matrix_dir(self, intermediates, A, flip=False):
278
+ """Materialize the SSM token-mixing matrix for one scan direction.
279
+
280
+ `intermediates` is the dict returned by `_scan_dir(...,
281
+ return_intermediates=True)`. The caller must pass the intermediates
282
+ in the scan's internal coordinates: for `flip=True`, `_scan_dir`
283
+ already flipped dt/B/C, so this function only flips the output matrix
284
+ back to the original sequence order.
285
+
286
+ Returns a [B, L, L, H] tensor where entry [b, t, s, h] is the
287
+ contribution of position s to position t for head h.
288
+ """
289
+ dt = intermediates["dt_effective"] # [B, L, H]
290
+ B = rearrange(intermediates["B"], "b l (g n) -> b l g n",
291
+ g=self.ngroups)
292
+ C = rearrange(intermediates["C"], "b l (g n) -> b l g n",
293
+ g=self.ngroups)
294
+ # Broadcast B/C from groups to heads
295
+ B_h = B.repeat_interleave(self.nheads // self.ngroups, dim=2) # [B,L,H,N]
296
+ C_h = C.repeat_interleave(self.nheads // self.ngroups, dim=2)
297
+ # log decay per step, per head, per state dim
298
+ log_decay = (dt.unsqueeze(-1) * # [B,L,H,1]
299
+ A.view(1, 1, self.nheads, 1).float()) # [1,1,H,1]
300
+ log_decay_cumsum = torch.cumsum(log_decay, dim=1) # [B,L,H,N]
301
+ B, L, H, N = B_h.shape
302
+ cum_t = log_decay_cumsum.unsqueeze(2) # [B,L,1,H,N]
303
+ cum_s = log_decay_cumsum.unsqueeze(1) # [B,1,L,H,N]
304
+ decay_diff = torch.exp(cum_t - cum_s) # [B,L,L,H,N]
305
+ # C_t @ (decay) @ B_s, summed over state dim
306
+ contrib = (C_h.unsqueeze(2) * B_h.unsqueeze(1) * decay_diff).sum(-1)
307
+ # multiply by dt_s
308
+ contrib = contrib * dt.unsqueeze(1) # [B,1,L,H]
309
+ # causal/anti-causal mask applied by caller via flip
310
+ tri = torch.tril(torch.ones(L, L, device=contrib.device)).view(1, L, L, 1)
311
+ L_mat = contrib * tri
312
+ if flip:
313
+ L_mat = L_mat.flip(1).flip(2)
314
+ return L_mat
315
+
316
+ def mixing_matrix(self, u, valid_mask=None, average_heads=True,
317
+ normalize_rows=False):
318
+ """Materialize the bidirectional SSM token-mixing matrix.
319
+
320
+ Returns a tensor of shape `[B, L, L]` (or `[B, L, L, H]` if
321
+ `average_heads=False`) where entry `[b, t, s]` is the SSM contribution
322
+ of input token s to output token t. The forward scan fills the lower
323
+ triangle and the backward scan fills the upper triangle.
324
+
325
+ This is the analogue of an attention map for SSD: it shows how far
326
+ each position attends to past/future tokens, and whether recency bias
327
+ is present (diagonal-near weights dominating).
328
+ """
329
+ A = -torch.exp(self.A_log.float())
330
+ A_b = -torch.exp(self.A_log_b.float())
331
+ z, xBC, xBC_b, dt, dt_b = self._split_inputs(u)
332
+ mask = None if valid_mask is None else valid_mask.unsqueeze(-1).to(xBC.dtype)
333
+ fw = self._scan_dir(xBC, dt, A, self.conv1d, self.D, self.dt_bias,
334
+ flip=False, mask=mask, return_intermediates=True)
335
+ bw = self._scan_dir(xBC_b, dt_b, A_b, self.conv1d_b, self.D_b,
336
+ self.dt_bias_b, flip=True, mask=mask,
337
+ return_intermediates=True)
338
+ L = self._mixing_matrix_dir(fw[1], A, flip=False) + \
339
+ self._mixing_matrix_dir(bw[1], A_b, flip=True)
340
+ if average_heads:
341
+ L = L.mean(-1) # [B,L,L]
342
+ if normalize_rows:
343
+ denom = L.abs().sum(dim=-1, keepdim=True).clamp_min(1e-12)
344
+ L = L / denom
345
+ return L.detach()
346
+
347
+ def forward(self, u, valid_mask=None, return_stats=False):
348
+ """`u`: [B, L, d_model]. `valid_mask`: [B, L] bool, True on real tokens.
349
+
350
+ Masked positions neither influence real ones nor carry an output; the
351
+ result on the real positions is identical to running them alone.
352
+
353
+ If `return_stats=True`, returns `(output, stats_dict)` where the stats
354
+ are detached diagnostics about dt, A decay, B/C norms, forward/backward
355
+ scan contributions, etc. This keeps intermediate tensors alive and is
356
+ intended for logging, not for every training step.
357
+ """
358
+ A = -torch.exp(self.A_log.float())
359
+ A_b = -torch.exp(self.A_log_b.float())
360
+ z, xBC, xBC_b, dt, dt_b = self._split_inputs(u)
361
+ mask = None if valid_mask is None else valid_mask.unsqueeze(-1).to(xBC.dtype)
362
+
363
+ if return_stats:
364
+ fw = self._scan_dir(xBC, dt, A, self.conv1d, self.D, self.dt_bias,
365
+ flip=False, mask=mask, return_intermediates=True)
366
+ bw = self._scan_dir(xBC_b, dt_b, A_b, self.conv1d_b, self.D_b,
367
+ self.dt_bias_b, flip=True, mask=mask,
368
+ return_intermediates=True)
369
+ y_fw, y_bw = fw[0], bw[0]
370
+ stats = self._compute_stats(z, A, A_b, fw, bw, mask=mask)
371
+ else:
372
+ y_fw = self._scan_dir(xBC, dt, A, self.conv1d, self.D, self.dt_bias,
373
+ flip=False, mask=mask)
374
+ y_bw = self._scan_dir(xBC_b, dt_b, A_b, self.conv1d_b, self.D_b,
375
+ self.dt_bias_b, flip=True, mask=mask)
376
+ stats = None
377
+
378
+ y = y_fw + y_bw
379
+ y = self.norm(rearrange(y, "b l h p -> b l (h p)"), z)
380
+ out = self.out_proj(y)
381
+ out = out if mask is None else out * mask
382
+ if return_stats:
383
+ return out, stats
384
+ return out
@@ -0,0 +1,58 @@
1
+ """Bidirectional Mamba-3 mixer (prototype).
2
+
3
+ Requires the latest ``state-spaces/mamba`` source install (mamba-ssm >= 2.3.x)
4
+ which provides ``mamba_ssm.modules.mamba3.Mamba3``. Only the SISO path is used
5
+ here because MIMO mode needs TileLang, which is not required for encoders.
6
+
7
+ This is intentionally a separate module so that the rest of the package still
8
+ loads on environments that only have Mamba-2 installed.
9
+ """
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+
14
+ from mamba_ssm.modules.mamba3 import Mamba3
15
+
16
+
17
+ class BiMamba3Mixer(nn.Module):
18
+ """Bidirectional Mamba-3 SISO mixer: forward scan + reversed backward scan.
19
+
20
+ Uses two independent ``Mamba3`` instances, so the parameter count is roughly
21
+ twice that of a single causal Mamba-3 block. This is a feasibility prototype;
22
+ a production version would share ``in_proj`` and separate only the
23
+ direction-dependent quantities (B, C, dt, A).
24
+ """
25
+
26
+ def __init__(self, d_model, d_state=64, headdim=64, expand=2,
27
+ chunk_size=64, dt_min=0.001, dt_max=0.1,
28
+ dt_init_floor=1e-4, A_floor=1e-4, rope_fraction=0.5,
29
+ device=None, dtype=None):
30
+ super().__init__()
31
+ factory_kwargs = {"device": device, "dtype": dtype}
32
+ self.d_model = d_model
33
+ self.d_inner = int(expand * d_model)
34
+ assert self.d_inner % headdim == 0
35
+ self.nheads = self.d_inner // headdim
36
+
37
+ common = {
38
+ "d_state": d_state,
39
+ "headdim": headdim,
40
+ "expand": expand,
41
+ "chunk_size": chunk_size,
42
+ "dt_min": dt_min,
43
+ "dt_max": dt_max,
44
+ "dt_init_floor": dt_init_floor,
45
+ "A_floor": A_floor,
46
+ "rope_fraction": rope_fraction,
47
+ "is_mimo": False,
48
+ "is_outproj_norm": False,
49
+ **factory_kwargs,
50
+ }
51
+ self.mamba_fw = Mamba3(d_model, **common)
52
+ self.mamba_bw = Mamba3(d_model, **common)
53
+
54
+ def forward(self, u):
55
+ """`u`: [B, L, d_model]. Returns: [B, L, d_model]."""
56
+ y_fw = self.mamba_fw(u)
57
+ y_bw = self.mamba_bw(u.flip(1)).flip(1)
58
+ return y_fw + y_bw
@@ -0,0 +1,53 @@
1
+ import torch.nn as nn
2
+
3
+ try:
4
+ from mamba_ssm.ops.triton.layer_norm import RMSNorm
5
+ except ImportError:
6
+ RMSNorm = nn.LayerNorm
7
+
8
+ from .block import BiMambaBlock, _NORM_EPSILON
9
+
10
+
11
+ class BiMambaStack(nn.Module):
12
+ """Stack of `n_layers` BiMambaBlocks + final RMSNorm.
13
+
14
+ Input/output: `[B, L, d_model]`.
15
+ """
16
+
17
+ def __init__(self, d_model, n_layers=4, d_state=64, headdim=64,
18
+ expand=2, chunk_size=49, dt_min=0.001, dt_max=0.02,
19
+ mlp_expand: float = 8 / 3, share_dt=False, share_bc=True,
20
+ device=None, dtype=None):
21
+ super().__init__()
22
+ factory_kwargs = {"device": device, "dtype": dtype}
23
+ self.layers = nn.ModuleList([
24
+ BiMambaBlock(
25
+ d_model, d_state=d_state, headdim=headdim, expand=expand,
26
+ chunk_size=chunk_size, dt_min=dt_min, dt_max=dt_max,
27
+ mlp_expand=mlp_expand, share_dt=share_dt, share_bc=share_bc,
28
+ **factory_kwargs,
29
+ )
30
+ for _ in range(n_layers)
31
+ ])
32
+ self.norm_f = RMSNorm(d_model, eps=_NORM_EPSILON, **factory_kwargs)
33
+
34
+ def forward(self, x, valid_mask=None, return_stats=False):
35
+ """`valid_mask`: [B, L] bool, True on real tokens. Required for padded
36
+ batches -- see `BiMambaMixer`.
37
+
38
+ If `return_stats=True`, returns `(output, stats_dict)` with per-layer
39
+ mixer diagnostics prefixed by `layer{i}/` plus `final_norm/...`.
40
+ """
41
+ stats = {} if return_stats else None
42
+ for i, layer in enumerate(self.layers):
43
+ if return_stats:
44
+ x, layer_stats = layer(x, valid_mask=valid_mask, return_stats=True)
45
+ stats[f"layer{i}/mixer"] = layer_stats["mixer"]
46
+ else:
47
+ x = layer(x, valid_mask=valid_mask)
48
+ x = self.norm_f(x)
49
+ if return_stats:
50
+ stats["final_norm/mean"] = x.mean().detach()
51
+ stats["final_norm/std"] = x.std(unbiased=False).detach()
52
+ return x, stats
53
+ return x
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ bidirectional_mamba/__init__.py
6
+ bidirectional_mamba/_mlp.py
7
+ bidirectional_mamba/block.py
8
+ bidirectional_mamba/mixer.py
9
+ bidirectional_mamba/mixer3.py
10
+ bidirectional_mamba/stack.py
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bidirectional-mamba"
7
+ version = "0.2.0"
8
+ description = "Bidirectional Mamba-2 SSD block with SwiGLU FFN (Vim/VMamba-style merge)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "redEddie" }]
13
+ keywords = ["mamba", "ssm", "bidirectional", "vision-mamba", "swiglu"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
17
+ ]
18
+ dependencies = [
19
+ "torch>=2.1",
20
+ "einops>=0.4",
21
+ "mamba-ssm>=2.0",
22
+ "causal-conv1d>=1.2",
23
+ "matplotlib>=3.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Repository = "https://github.com/redEddie/bidirectional-mamba"
28
+
29
+ [tool.setuptools.packages.find]
30
+ include = ["bidirectional_mamba*"]
31
+ exclude = ["tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+