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.
- bidirectional_mamba-0.2.0/LICENSE +21 -0
- bidirectional_mamba-0.2.0/MANIFEST.in +17 -0
- bidirectional_mamba-0.2.0/PKG-INFO +179 -0
- bidirectional_mamba-0.2.0/README.md +159 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba/__init__.py +20 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba/_mlp.py +16 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba/block.py +48 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba/mixer.py +384 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba/mixer3.py +58 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba/stack.py +53 -0
- bidirectional_mamba-0.2.0/bidirectional_mamba.egg-info/SOURCES.txt +10 -0
- bidirectional_mamba-0.2.0/pyproject.toml +31 -0
- bidirectional_mamba-0.2.0/setup.cfg +4 -0
|
@@ -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,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*"]
|