arianraje commited on
Commit
e861880
·
verified ·
1 Parent(s): b8090fa

remote-code bundle

Browse files
Files changed (1) hide show
  1. modeling_qwen3_mamba3.py +1002 -0
modeling_qwen3_mamba3.py ADDED
@@ -0,0 +1,1002 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED FILE -- do not edit.
2
+ #
3
+ # Produced by `scripts/mamba3/make_hf_remote_code_bundle.py` from
4
+ # `src/models/qwen3_mamba3.py` in the Linearization repo (branch mamba3). Edit that
5
+ # file and regenerate. Carries the config and every model class; config.json's
6
+ # `auto_map` points AutoConfig and AutoModelForCausalLM here.
7
+ #
8
+ # The fused Mamba-3 MIMO kernel (state-spaces/mamba at e9594ce plus the local
9
+ # backward fix described in the model card) is optional: without it every mixer
10
+ # runs `mamba3_mimo_reference`, a sequential fp32 torch path that is exact but slow.
11
+
12
+ """Qwen3 -> Mamba-3 (MIMO) hybrid container.
13
+
14
+ The Mamba-3 sibling of ``src/models/qwen3_mamba2.py``: retained layers are literal
15
+ ``Qwen3DecoderLayer``s (bit-exact retention, no masquerade), converted layers mount a
16
+ Mamba-3 MIMO mixer under the load-bearing attribute name ``linear_attn`` (all four
17
+ trainers split LR groups on the ``".linear_attn."`` substring, and
18
+ ``stage1_align.py::layer_forward`` calls ``layer.linear_attn(hidden_states=...)`` and
19
+ expects a bare tensor back).
20
+
21
+ Why a hand-written mixer rather than the upstream ``mamba_ssm.modules.mamba3.Mamba3``:
22
+
23
+ * upstream's module is inseparable from its kernels -- it asserts the TileLang MIMO
24
+ kernel imported at construction and has no torch fallback -- so nothing built on it
25
+ could run the CPU surgery tests, the login-node surgery, or a cached decode step
26
+ (its ``step()`` needs the CuteDSL ``mamba3_step_fn``, which needs ``quack``, which
27
+ this env does not have);
28
+ * its ``_preprocess`` for decode does ``B.expand(-1, -1, nheads, -1)``, which is only
29
+ valid for ``ngroups == 1``; we run 8 B/C groups (one per teacher KV head);
30
+ * every init choice here is deliberate (see ``Qwen3Mamba3Mixer.init_ssm_parameters``)
31
+ and several differ from upstream's -- the whole point of the surgery is that the
32
+ mixer starts as the teacher's attention head, not as a random SSM.
33
+
34
+ The **math and the parameter layout are upstream's exactly** (mamba_ssm @ e9594ce,
35
+ ``mamba_ssm/modules/mamba3.py``): same ``in_proj`` row order
36
+ ``[z | x | B | C | dd_dt | dd_A | trap | angles]``, same rank-major ``(r g n)`` B/C
37
+ layout, same ``dt_bias``/``B_bias``/``C_bias``/``mimo_x``/``mimo_z``/``mimo_o``/``D``/
38
+ ``norm.weight`` parameter names and shapes, and the prefill calls upstream's fused
39
+ TileLang kernel ``mamba3_mimo`` untouched. The pure-torch path
40
+ (``mamba3_mimo_reference``) is a transcription of the kernel's own semantics -- the
41
+ rotate-half pairing ``(n, N/2 + n)`` for ``n < N // rotary_dim_divisor``, bias added
42
+ before rotation, exponential-trapezoidal state update, per-(head, rank) pre-gate
43
+ RMSNorm, ``mimo_o`` reduction -- and is checked against the kernel on a GPU in
44
+ ``tests/test_qwen3_mamba3_surgery.py``.
45
+
46
+ Importing this module is CPU-safe: the kernel import is lazy and only attempted for
47
+ CUDA tensors.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import math
53
+ import warnings
54
+ from typing import Optional
55
+
56
+ import torch
57
+ import torch.nn.functional as F
58
+ from torch import nn
59
+
60
+ from transformers.generation import GenerationMixin
61
+ from transformers.masking_utils import create_causal_mask
62
+ from transformers.modeling_layers import GradientCheckpointingLayer
63
+ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
64
+ from transformers.modeling_utils import PreTrainedModel
65
+ from transformers.models.bamba.modeling_bamba import HybridMambaAttentionDynamicCache
66
+ from transformers.models.qwen3.configuration_qwen3 import Qwen3Config
67
+ from transformers.models.qwen3.modeling_qwen3 import (
68
+ Qwen3DecoderLayer,
69
+ Qwen3MLP,
70
+ Qwen3RMSNorm,
71
+ Qwen3RotaryEmbedding,
72
+ )
73
+
74
+ # Pinned upstream: github.com/state-spaces/mamba @ e9594ce1c732d97440f0332fdc43170a2294dbfa
75
+ # plus one local commit in the staged clone, cc12e795314d31a2e68e1407bd4309a91673bc23:
76
+ # mamba3_mimo backward reads ``ctx.saved_tensors`` once, so it survives HF non-reentrant
77
+ # activation checkpointing (stage 2 KD). Stage 1 never checkpoints, so it never hit this.
78
+ # (staged at /mnt/home/afei1/mamba3_upstream; scripts/beta/_env_b200_mamba3.sh prepends
79
+ # it to PYTHONPATH so the whole ``mamba_ssm`` package resolves there).
80
+ UPSTREAM_SHA = "cc12e795314d31a2e68e1407bd4309a91673bc23" # e9594ce + local checkpoint-compat fix
81
+
82
+
83
+ class Qwen3Mamba3Config(Qwen3Config):
84
+ """Qwen3 config + the Mamba-3 MIMO parameterization + hybrid ``layer_types``.
85
+
86
+ The Qwen3-4B backbone is restated as defaults for the same reason as in the
87
+ Mamba2 container: ``PretrainedConfig.to_diff_dict`` builds a zero-arg instance to
88
+ diff against, and the consistency checks below must hold for that instance or
89
+ ``save_pretrained`` raises.
90
+
91
+ Geometry (Qwen3-4B, hidden 2560, 32 q heads x 128, 8 kv heads):
92
+
93
+ * ``mamba_n_heads`` = 32 = teacher q heads; ``mamba_d_head`` = 128 (P, the value
94
+ width); ``mamba_d_state`` = 128 (N, the key width -- scores are C . B).
95
+ * ``mamba_n_groups`` = 8 = teacher kv heads. Head h reads B/C group ``h // (H/G)``
96
+ (``mamba3_mimo_fwd.py:142``, ``i_h_qk = i_h // (H // G)``), which is exactly
97
+ ``repeat_kv``: the four heads of a group share one key, like GQA.
98
+ * ``mamba_mimo_rank`` = 4 = gqa. Rank r of group g carries teacher q head
99
+ ``4g + r``: the four q heads that share a kv head become the four MIMO ranks
100
+ of that group, so the MIMO structure is *populated by GQA* rather than by
101
+ random init. Any R with ``R | gqa`` or ``gqa | R`` is legal (heads are
102
+ averaged or replicated across ranks); R=2 is the parameter-matched control.
103
+ * ``mamba_rope_fraction`` in {0.5, 1.0}: fraction of N carried through the
104
+ data-dependent rotation (kernel supports divisors 4 and 2 only). Only 0.5 is
105
+ tested upstream.
106
+ * ``mamba_chunk_size``: kernel chunk. The kernels tile ``fused = chunk * R`` rows and
107
+ the backward-of-backward kernel holds every tile in shared memory at once
108
+ (:func:`mamba3_bwd_smem_bytes`); at N=P=128 that overflows the 227 KiB a B200/H100
109
+ block may use as soon as ``fused > 32`` (chunk 16 x R=4 asked for 281248 B and did
110
+ not compile in the first stage-1 gate, job 196032). From below, ``fused < 32`` does
111
+ not compile either: the kernels' ``fused x fused`` GEMMs must split across the
112
+ block's 4 warps and TileLang's MMA warp partition cannot tile 16 x 16 four ways
113
+ (chunk 8 x R=2, job 196045). So at N=P=128 ``chunk * R == 32`` exactly
114
+ (:data:`MAMBA3_MIN_FUSED_TILE`): default 8 for R=4; R=2 uses 16
115
+ (:func:`mamba3_max_chunk_size`). Upstream's ``64 / R`` advice assumes its headdim
116
+ 64. Tiling only: the math is chunk-invariant. The kernel asserts ``>= 8``.
117
+ * ``mamba_bc_bias_init``: value the B/C biases start at. Upstream uses 1.0. The
118
+ worry that on an inherited init this adds ``sum(q) + sum(k) + N`` to every
119
+ attention score and swamps the teacher's structure was tested (bias 0 vs 1, same
120
+ surgery): bias 1 led at every stage-1 and stage-2a eval (0.00376 vs 0.00397 rel-MSE,
121
+ 0.1258 vs 0.1297 KL), so the default is upstream's 1.0.
122
+ """
123
+
124
+ model_type = "qwen3_mamba3"
125
+
126
+ def __init__(
127
+ self,
128
+ # --- Qwen3-4B backbone, restated so the zero-arg constructor is valid ---
129
+ vocab_size: int = 151936,
130
+ hidden_size: int = 2560,
131
+ intermediate_size: int = 9728,
132
+ num_hidden_layers: int = 36,
133
+ num_attention_heads: int = 32,
134
+ num_key_value_heads: int = 8,
135
+ head_dim: int = 128,
136
+ # --- Mamba-3 MIMO parameterization ---
137
+ mamba_n_heads: int = 32,
138
+ mamba_d_head: int = 128,
139
+ mamba_d_state: int = 128,
140
+ mamba_n_groups: int = 8,
141
+ mamba_mimo_rank: int = 4,
142
+ mamba_expand: float = 1.6, # int(1.6 * 2560) == 4096 exactly
143
+ mamba_chunk_size: int = 8,
144
+ mamba_rope_fraction: float = 0.5,
145
+ mamba_bc_bias_init: float = 1.0,
146
+ mamba_a_floor: float = 1e-4,
147
+ mamba_dt_min: float = 0.001,
148
+ mamba_dt_max: float = 0.1,
149
+ mamba_dt_init_floor: float = 1e-4,
150
+ mamba_norm_eps: float = 1e-5, # pre-gate per-head RMSNorm (upstream 1e-5)
151
+ mamba_bc_norm_eps: Optional[float] = None, # None -> rms_norm_eps (teacher q/k_norm)
152
+ **kwargs,
153
+ ):
154
+ super().__init__(
155
+ vocab_size=vocab_size,
156
+ hidden_size=hidden_size,
157
+ intermediate_size=intermediate_size,
158
+ num_hidden_layers=num_hidden_layers,
159
+ num_attention_heads=num_attention_heads,
160
+ num_key_value_heads=num_key_value_heads,
161
+ head_dim=head_dim,
162
+ **kwargs,
163
+ )
164
+ self.mamba_n_heads = mamba_n_heads
165
+ self.mamba_d_head = mamba_d_head
166
+ self.mamba_d_state = mamba_d_state
167
+ self.mamba_n_groups = mamba_n_groups
168
+ self.mamba_mimo_rank = mamba_mimo_rank
169
+ self.mamba_expand = mamba_expand
170
+ self.mamba_chunk_size = mamba_chunk_size
171
+ self.mamba_rope_fraction = mamba_rope_fraction
172
+ self.mamba_bc_bias_init = mamba_bc_bias_init
173
+ self.mamba_a_floor = mamba_a_floor
174
+ self.mamba_dt_min = mamba_dt_min
175
+ self.mamba_dt_max = mamba_dt_max
176
+ self.mamba_dt_init_floor = mamba_dt_init_floor
177
+ self.mamba_norm_eps = mamba_norm_eps
178
+ self.mamba_bc_norm_eps = mamba_bc_norm_eps
179
+
180
+ got = int(self.mamba_expand * self.hidden_size)
181
+ want = self.mamba_n_heads * self.mamba_d_head
182
+ if got != want:
183
+ raise ValueError(
184
+ f"int(mamba_expand * hidden_size) = int({self.mamba_expand} * "
185
+ f"{self.hidden_size}) = {got}, but mamba_n_heads * mamba_d_head = "
186
+ f"{self.mamba_n_heads} * {self.mamba_d_head} = {want}. These must be "
187
+ "equal for out_proj to inherit the teacher's o_proj verbatim."
188
+ )
189
+ if self.mamba_n_heads % self.mamba_n_groups != 0:
190
+ raise ValueError(
191
+ f"mamba_n_heads ({self.mamba_n_heads}) must be divisible by "
192
+ f"mamba_n_groups ({self.mamba_n_groups}): head h reads B/C group "
193
+ "h // (H/G), exactly the way repeat_kv shares GQA kv heads."
194
+ )
195
+ hpg = self.mamba_n_heads // self.mamba_n_groups
196
+ R = self.mamba_mimo_rank
197
+ if R < 1 or not (hpg % R == 0 or R % hpg == 0):
198
+ raise ValueError(
199
+ f"mamba_mimo_rank ({R}) must divide or be a multiple of heads-per-group "
200
+ f"({hpg}) so ranks map onto the teacher's q heads of a kv group"
201
+ )
202
+ if self.mamba_rope_fraction not in (0.5, 1.0):
203
+ raise ValueError("mamba_rope_fraction must be 0.5 or 1.0 (kernel divisors 4/2)")
204
+ if self.mamba_d_state % 2 != 0 or mamba3_num_rope_angles(self) < 1:
205
+ raise ValueError(f"mamba_d_state ({self.mamba_d_state}) must be even and >= 4")
206
+ if self.mamba_chunk_size < 8:
207
+ raise ValueError("mamba_chunk_size must be >= 8 (kernel assertion)")
208
+ fused = self.mamba_chunk_size * self.mamba_mimo_rank
209
+ if fused < MAMBA3_MIN_FUSED_TILE:
210
+ raise ValueError(
211
+ f"mamba_chunk_size={self.mamba_chunk_size} x mamba_mimo_rank={self.mamba_mimo_rank} = "
212
+ f"fused tile {fused} < {MAMBA3_MIN_FUSED_TILE}: the kernels' fused x fused GEMMs cannot be "
213
+ f"split across a block's 4 warps (TileLang warp partition; job 196045). Use chunk >= "
214
+ f"{-(-MAMBA3_MIN_FUSED_TILE // self.mamba_mimo_rank)}"
215
+ )
216
+ div = mamba3_rotary_dim_divisor(self)
217
+ need = mamba3_bwd_smem_bytes(self.mamba_d_state, self.mamba_d_head, self.mamba_chunk_size,
218
+ self.mamba_mimo_rank, div)
219
+ if need > MAMBA3_MAX_DYN_SMEM_BYTES:
220
+ fit = mamba3_max_chunk_size(self.mamba_d_state, self.mamba_d_head, self.mamba_mimo_rank, div)
221
+ raise ValueError(
222
+ f"mamba_chunk_size={self.mamba_chunk_size} x mamba_mimo_rank={self.mamba_mimo_rank} needs "
223
+ f"{need} B of shared memory in the MIMO backward kernel at N={self.mamba_d_state}, "
224
+ f"P={self.mamba_d_head}; a block may use {MAMBA3_MAX_DYN_SMEM_BYTES} B (227 KiB). "
225
+ f"Largest chunk that fits: {fit}"
226
+ )
227
+
228
+ @property
229
+ def layers_block_type(self) -> list[str]:
230
+ """Bamba's vocabulary for ``layer_types`` ("mamba"/"attention")."""
231
+ return ["mamba" if t == "linear_attention" else "attention" for t in self.layer_types]
232
+
233
+
234
+ # Largest dynamic shared memory one block may opt into on sm_90 / sm_100 (227 KiB).
235
+ MAMBA3_MAX_DYN_SMEM_BYTES = 227 * 1024
236
+ # Smallest fused tile (chunk * R) the kernels compile at: every kernel runs 128 threads =
237
+ # 4 warps and TileLang's default MMA warp partition of a fused x fused GEMM needs
238
+ # m_warp * n_warp == 4, which a 16 x 16 tile cannot give (1 x 1; "m_warp * n_warp must
239
+ # equal num_warps", job 196045 at chunk 8 x R=2). 32 x 32 splits 2 x 2 and is what both
240
+ # production geometries run.
241
+ MAMBA3_MIN_FUSED_TILE = 32
242
+
243
+
244
+ def mamba3_bwd_smem_bytes(d_state: int, d_head: int, chunk_size: int, mimo_rank: int,
245
+ rotary_dim_divisor: int = 4) -> int:
246
+ """Shared memory (bytes) upstream's ``mamba_mimo_bwd_bwd`` TileLang kernel allocates per block.
247
+
248
+ Mirrors its ``alloc_shared`` list (mamba3_mimo_bwd.py; ``fused = chunk * R``; bf16 tiles
249
+ unless noted): states, dstates [N,P]; dout, v [chunk,P]; dPhiO, PsiV, dPsiV_combined
250
+ [fused,P]; q, k, q_pre_rot, k_pre_rot, dq, dk, k_pre_trap [fused,N]; lkq_masked and its
251
+ dtype buffer [fused,fused]; dqk_from_diag [fused,fused] fp32; dangle [fused,N/div] fp32;
252
+ qk_dot [chunk,R,R]; trap_scale [chunk]; dA_cs, dA_cs_rev [chunk] fp32; plus 2048 B the
253
+ compiler adds (modeled 279200 vs the 281248 the driver refused at N=P=128, chunk 16,
254
+ R=4). Upstream pipelines with ``num_stages=0``, so there is no multiplier.
255
+ """
256
+ N, P, c, R = d_state, d_head, chunk_size, mimo_rank
257
+ f = c * R
258
+ bf, f32 = 2, 4
259
+ b = 2 * N * P * bf # states, dstates
260
+ b += 2 * c * P * bf # dout, v
261
+ b += 3 * f * P * bf # dPhiO, PsiV, dPsiV_combined
262
+ b += 7 * f * N * bf # q, k, q/k_pre_rot, dq, dk, k_pre_trap
263
+ b += 2 * f * f * bf + f * f * f32 # lkq_masked (+ dtype buf), dqk_from_diag
264
+ b += f * (N // rotary_dim_divisor) * f32 # dangle
265
+ b += c * R * R * bf + c * bf + 2 * c * f32 # qk_dot, trap_scale, dA_cs, dA_cs_rev
266
+ return b + 2048
267
+
268
+
269
+ def mamba3_max_chunk_size(d_state: int, d_head: int, mimo_rank: int, rotary_dim_divisor: int = 4,
270
+ headroom: int = 8 * 1024) -> int | None:
271
+ """Largest power-of-two chunk in [8, 256] whose backward kernel fits with ``headroom`` spare
272
+ and whose fused tile ``chunk * R`` reaches :data:`MAMBA3_MIN_FUSED_TILE`.
273
+
274
+ N=P=128: 8 for R=4, 16 for R=2, 32 for R=1, None for R=8 (fused 64 overflows at any
275
+ chunk >= 8).
276
+ """
277
+ best, c = None, 8
278
+ while c <= 256:
279
+ if c * mimo_rank >= MAMBA3_MIN_FUSED_TILE:
280
+ if mamba3_bwd_smem_bytes(d_state, d_head, c, mimo_rank, rotary_dim_divisor) + headroom > MAMBA3_MAX_DYN_SMEM_BYTES:
281
+ break
282
+ best = c
283
+ c *= 2
284
+ return best
285
+
286
+
287
+ def mamba3_rotary_dim_divisor(config) -> int:
288
+ return int(round(2 / config.mamba_rope_fraction))
289
+
290
+
291
+ def mamba3_num_rope_angles(config) -> int:
292
+ """Upstream: ``split = int(d_state * rope_fraction)`` (made even), angles = split // 2."""
293
+ split = int(config.mamba_d_state * config.mamba_rope_fraction)
294
+ if split % 2:
295
+ split -= 1
296
+ return split // 2
297
+
298
+
299
+ def rank_head_offsets(heads_per_group: int, mimo_rank: int) -> list[list[int]]:
300
+ """For each MIMO rank, the teacher q-head offsets (within one kv group) it carries.
301
+
302
+ R == hpg: rank r <-> q head offset r (the production case, 4 == 4).
303
+ R < hpg: rank r averages offsets ``[r*m, (r+1)*m)`` with m = hpg // R.
304
+ R > hpg: offset j is replicated onto ranks ``[j*m, (j+1)*m)`` with m = R // hpg.
305
+ Shared by the surgery (which writes the C rows) and ``mimo_o`` init (which reads
306
+ them back out), so the two cannot disagree.
307
+ """
308
+ hpg, R = heads_per_group, mimo_rank
309
+ if R <= hpg:
310
+ if hpg % R:
311
+ raise ValueError(f"mimo_rank {R} does not divide heads-per-group {hpg}")
312
+ m = hpg // R
313
+ return [list(range(r * m, (r + 1) * m)) for r in range(R)]
314
+ if R % hpg:
315
+ raise ValueError(f"mimo_rank {R} is not a multiple of heads-per-group {hpg}")
316
+ m = R // hpg
317
+ return [[r // m] for r in range(R)]
318
+
319
+
320
+ def own_rank_matrix(num_heads: int, n_groups: int, mimo_rank: int) -> torch.Tensor:
321
+ """``(H, R)`` read-out weights making head h output *its own* teacher head.
322
+
323
+ Head h = g*hpg + j is teacher attention head h; its query lives on the rank(s)
324
+ whose offsets include j. Entry ``[h, r]`` is ``1/len(own ranks)`` on those ranks
325
+ and 0 elsewhere, so at init ``mimo_o`` selects the inherited head instead of
326
+ averaging the group's four q heads into every head (upstream's ``ones / R``).
327
+ """
328
+ hpg = num_heads // n_groups
329
+ offsets = rank_head_offsets(hpg, mimo_rank)
330
+ mo = torch.zeros(num_heads, mimo_rank)
331
+ for h in range(num_heads):
332
+ j = h % hpg
333
+ own = [r for r in range(mimo_rank) if j in offsets[r]]
334
+ mo[h, own] = 1.0 / len(own)
335
+ return mo
336
+
337
+
338
+ def heavy_tail_activation(x: torch.Tensor) -> torch.Tensor:
339
+ """Upstream ``heavy_tail_activation``: identity for x >= 0, ``1 / (1 - x)`` for x < 0.
340
+
341
+ Continuous at 0 with value 1 and slope 1 on both sides, so zero-initialized
342
+ ``dd_A`` rows give A = -1 for every head and a live gradient in both directions.
343
+ """
344
+ return x.clamp(min=0) + 1.0 / (1.0 - x.clamp(max=0))
345
+
346
+
347
+ def _rotate_half_partial(t: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
348
+ """Kernel-aligned rotation on the last dim: for n < A, pair ``(n, N/2 + n)``.
349
+
350
+ ``t``: (..., N); ``cos``/``sin``: broadcastable to (..., A) with A <= N/2.
351
+ Dims outside the two rotated slices pass through unchanged (rope_fraction 0.5).
352
+ """
353
+ N = t.shape[-1]
354
+ A = cos.shape[-1]
355
+ half = N // 2
356
+ t0, mid, t1, rest = t[..., :A], t[..., A:half], t[..., half : half + A], t[..., half + A :]
357
+ r0 = t0 * cos - t1 * sin
358
+ r1 = t0 * sin + t1 * cos
359
+ return torch.cat([r0, mid, r1, rest], dim=-1)
360
+
361
+
362
+ def mamba3_mimo_reference(
363
+ C: torch.Tensor,
364
+ B: torch.Tensor,
365
+ x: torch.Tensor,
366
+ z: torch.Tensor,
367
+ adt: torch.Tensor,
368
+ dt: torch.Tensor,
369
+ trap: torch.Tensor,
370
+ C_bias: torch.Tensor,
371
+ B_bias: torch.Tensor,
372
+ mimo_x: torch.Tensor,
373
+ mimo_z: torch.Tensor,
374
+ mimo_o: torch.Tensor,
375
+ angles: torch.Tensor,
376
+ D: torch.Tensor,
377
+ norm_weight: torch.Tensor,
378
+ norm_eps: float,
379
+ initial_states: Optional[tuple] = None,
380
+ ) -> tuple[torch.Tensor, tuple]:
381
+ """Sequential fp32 transcription of the fused kernel (prefill *and* decode).
382
+
383
+ Shapes (b batch, l tokens, R ranks, G groups, H heads, N state, P head width,
384
+ A rope angles):
385
+ C, B: (b, l, R, G, N) post-RMSNorm, pre-bias x, z: (b, l, H, P)
386
+ adt, dt: (b, H, l) fp32 trap: (b, H, l) pre-sigmoid
387
+ C_bias, B_bias: (H, R, N) mimo_*: (H, R, P) angles: (b, l, H, A) raw
388
+ D: (H,) norm_weight: (H*P,)
389
+ initial_states: (angle (b,H,A) f32, ssm (b,H,P,N) f32, k (b,R,H,N), v_raw (b,H,P))
390
+ exactly what the kernel returns with ``return_state=True`` -- ``k`` is the
391
+ last token's biased+rotated key and ``v_raw`` its *un-projected* x.
392
+
393
+ Per token t (upstream ``mamba3_MIMO_step_ref``, kernel rotate-half convention):
394
+ angle += tanh(angles_t) * dt_t * pi
395
+ q, k = rot(C_t + C_bias), rot(B_t + B_bias) v_r = x_t * mimo_x[:, r]
396
+ alpha = exp(adt_t); gamma = sigmoid(trap_t) * dt_t; beta = (1 - sigmoid) * dt_t * alpha
397
+ S = alpha * S + beta * (k_{t-1}^T v_{t-1}) + gamma * (k_t^T v_t) [sum over ranks]
398
+ y_r = S q_r + D * v_r -> RMSNorm_P * w * silu(z_t * mimo_z[:, r])
399
+ y = sum_r mimo_o[:, r] * y_r
400
+ Returns ``(y (b, l, H, P) fp32, final states in the kernel's layout)``.
401
+ """
402
+ b, l, R, G, N = C.shape
403
+ H, P = x.shape[2], x.shape[3]
404
+ hpg = H // G
405
+ A = angles.shape[-1]
406
+ half = N // 2
407
+ if A > half:
408
+ raise ValueError(f"{A} rope angles exceed d_state/2 = {half}")
409
+
410
+ # head h reads group h // hpg (kernel: i_h_qk = i_h // (H // G))
411
+ Cf = C.float().repeat_interleave(hpg, dim=3) + C_bias.float().permute(1, 0, 2)[None, None]
412
+ Bf = B.float().repeat_interleave(hpg, dim=3) + B_bias.float().permute(1, 0, 2)[None, None]
413
+ xf, zf = x.float(), z.float()
414
+ v_all = torch.einsum("blhp,hrp->blhrp", xf, mimo_x.float())
415
+ z_all = torch.einsum("blhp,hrp->blhrp", zf, mimo_z.float())
416
+ dt = dt.float()
417
+ sig = torch.sigmoid(trap.float())
418
+ alpha = torch.exp(adt.float())
419
+ gamma = sig * dt
420
+ beta = (1.0 - sig) * dt * alpha
421
+ w = norm_weight.float().view(H, P)
422
+ Df = D.float()
423
+ angles = angles.float()
424
+
425
+ if initial_states is not None:
426
+ angle_state, S, k_prev, v_prev_raw = initial_states
427
+ angle_state = angle_state.float().clone()
428
+ S = S.float().clone()
429
+ k_prev = k_prev.float().permute(0, 2, 1, 3) # (b, H, R, N)
430
+ v_prev = torch.einsum("bhp,hrp->bhrp", v_prev_raw.float(), mimo_x.float())
431
+ else:
432
+ angle_state = torch.zeros(b, H, A, dtype=torch.float32, device=x.device)
433
+ S = torch.zeros(b, H, P, N, dtype=torch.float32, device=x.device)
434
+ k_prev = torch.zeros(b, H, R, N, dtype=torch.float32, device=x.device)
435
+ v_prev = torch.zeros(b, H, R, P, dtype=torch.float32, device=x.device)
436
+
437
+ ys = []
438
+ for t in range(l):
439
+ q = Cf[:, t].permute(0, 2, 1, 3) # (b, H, R, N)
440
+ k = Bf[:, t].permute(0, 2, 1, 3)
441
+ v = v_all[:, t] # (b, H, R, P)
442
+ angle_state = angle_state + torch.tanh(angles[:, t]) * dt[:, :, t, None] * math.pi
443
+ cos = angle_state.cos()[:, :, None, :]
444
+ sin = angle_state.sin()[:, :, None, :]
445
+ q = _rotate_half_partial(q, cos, sin)
446
+ k = _rotate_half_partial(k, cos, sin)
447
+ prev_kv = torch.einsum("bhrn,bhrp->bhpn", k_prev, v_prev)
448
+ curr_kv = torch.einsum("bhrn,bhrp->bhpn", k, v)
449
+ S = (
450
+ alpha[:, :, t, None, None] * S
451
+ + beta[:, :, t, None, None] * prev_kv
452
+ + gamma[:, :, t, None, None] * curr_kv
453
+ )
454
+ out = torch.einsum("bhpn,bhrn->bhrp", S, q) + Df[None, :, None, None] * v
455
+ out = out * torch.rsqrt(out.square().mean(-1, keepdim=True) + norm_eps)
456
+ out = out * w[None, :, None, :] * F.silu(z_all[:, t])
457
+ ys.append(torch.einsum("bhrp,hrp->bhp", out, mimo_o.float()))
458
+ k_prev, v_prev = k, v
459
+
460
+ y = torch.stack(ys, dim=1)
461
+ angle_state = torch.remainder(angle_state, 2 * math.pi) # kernel's Final_Angle
462
+ finals = (angle_state, S, k_prev.permute(0, 2, 1, 3), xf[:, -1])
463
+ return y, finals
464
+
465
+
466
+ class Mamba3GatedRMSNorm(nn.Module):
467
+ """Pre-gate grouped RMSNorm: ``norm_P(y) * w * silu(gate)`` per head of ``group_size``.
468
+
469
+ Same function as upstream's ``RMSNormGated(d_inner, group_size=headdim,
470
+ norm_before_gate=True)`` -- the "removed RMSNorm layer [that] is crucial for
471
+ long-context extrapolation" in the paper's hybrid ablation -- and the same
472
+ parameter name/shape (``norm.weight``, ``(d_inner,)``). The fused kernel applies
473
+ it inside the MIMO reduction (per rank, then ``mimo_o``), so this ``forward`` only
474
+ serves the non-fused rank-collapsed form; the reference path inlines the per-rank
475
+ version.
476
+ """
477
+
478
+ def __init__(self, hidden_size: int, group_size: int, eps: float = 1e-5):
479
+ super().__init__()
480
+ if hidden_size % group_size:
481
+ raise ValueError(f"{hidden_size} not divisible by group_size {group_size}")
482
+ self.weight = nn.Parameter(torch.ones(hidden_size))
483
+ self.eps = eps
484
+ self.group_size = group_size
485
+
486
+ def forward(self, hidden_states: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
487
+ input_dtype = hidden_states.dtype
488
+ hs = hidden_states.float().unflatten(-1, (-1, self.group_size))
489
+ hs = hs * torch.rsqrt(hs.pow(2).mean(-1, keepdim=True) + self.eps)
490
+ hs = hs.flatten(-2) * self.weight.float() * F.silu(gate.float())
491
+ return hs.to(input_dtype)
492
+
493
+
494
+ _KERNEL = None # None: not tried yet; False: unavailable; else the callable
495
+
496
+
497
+ def load_mimo_kernel(strict: bool = False):
498
+ """Lazily import upstream's fused ``mamba3_mimo`` (TileLang). Cached per process."""
499
+ global _KERNEL
500
+ if _KERNEL is None:
501
+ try:
502
+ from mamba_ssm.ops.tilelang.mamba3.mamba3_mimo import mamba3_mimo
503
+
504
+ _KERNEL = mamba3_mimo
505
+ except Exception as e: # noqa: BLE001 - any import failure means "no kernel"
506
+ _KERNEL = False
507
+ msg = f"Mamba-3 MIMO kernel unavailable ({type(e).__name__}: {e}); using the torch reference"
508
+ if strict:
509
+ raise RuntimeError(msg) from e
510
+ warnings.warn(msg)
511
+ if strict and _KERNEL is False:
512
+ raise RuntimeError("Mamba-3 MIMO kernel unavailable (import failed earlier in this process)")
513
+ return _KERNEL
514
+
515
+
516
+ class Qwen3Mamba3Mixer(nn.Module):
517
+ """Mamba-3 MIMO token mixer, upstream layout, inherited-attention init.
518
+
519
+ Set ``Qwen3Mamba3Mixer.force_reference = True`` to bypass the kernel (tests).
520
+ """
521
+
522
+ force_reference = False
523
+
524
+ def __init__(self, config: Qwen3Mamba3Config, layer_idx: int):
525
+ super().__init__()
526
+ self.layer_idx = layer_idx
527
+ self.hidden_size = config.hidden_size
528
+ self.num_heads = config.mamba_n_heads
529
+ self.head_dim = config.mamba_d_head
530
+ self.ssm_state_size = config.mamba_d_state
531
+ self.n_groups = config.mamba_n_groups
532
+ self.mimo_rank = config.mamba_mimo_rank
533
+ self.intermediate_size = int(config.mamba_expand * config.hidden_size)
534
+ self.chunk_size = config.mamba_chunk_size
535
+ self.a_floor = config.mamba_a_floor
536
+ self.rotary_dim_divisor = mamba3_rotary_dim_divisor(config)
537
+ self.num_rope_angles = mamba3_num_rope_angles(config)
538
+ self.dt_min, self.dt_max = config.mamba_dt_min, config.mamba_dt_max
539
+ self.dt_init_floor = config.mamba_dt_init_floor
540
+ self.bc_bias_init = config.mamba_bc_bias_init
541
+ H, R, N, P, G = self.num_heads, self.mimo_rank, self.ssm_state_size, self.head_dim, self.n_groups
542
+ bc_eps = config.mamba_bc_norm_eps if config.mamba_bc_norm_eps is not None else config.rms_norm_eps
543
+
544
+ # [z | x | B | C | dd_dt | dd_A | trap | angles] (upstream mamba3.py:112)
545
+ self.split_sizes = [
546
+ self.intermediate_size, self.intermediate_size,
547
+ N * G * R, N * G * R,
548
+ H, H, H,
549
+ self.num_rope_angles,
550
+ ]
551
+ self.in_proj = nn.Linear(self.hidden_size, sum(self.split_sizes), bias=False)
552
+ self.dt_bias = nn.Parameter(torch.ones(H))
553
+ self.dt_bias._no_weight_decay = True
554
+ self.B_bias = nn.Parameter(torch.zeros(H, R, N))
555
+ self.C_bias = nn.Parameter(torch.zeros(H, R, N))
556
+ # Qwen3RMSNorm rather than upstream's RMSNormGated: identical function (fp32
557
+ # rstd, weight multiply), and with eps = rms_norm_eps it IS the teacher's
558
+ # q_norm/k_norm, so inherited weights reproduce the teacher's normalized q/k
559
+ # exactly rather than to 1e-5.
560
+ self.B_norm = Qwen3RMSNorm(N, eps=bc_eps)
561
+ self.C_norm = Qwen3RMSNorm(N, eps=bc_eps)
562
+ self.mimo_x = nn.Parameter(torch.full((H, R, P), 1.0 / R))
563
+ self.mimo_z = nn.Parameter(torch.ones(H, R, P))
564
+ self.mimo_o = nn.Parameter(torch.full((H, R, P), 1.0 / R))
565
+ self.D = nn.Parameter(torch.ones(H))
566
+ self.D._no_weight_decay = True
567
+ self.norm = Mamba3GatedRMSNorm(self.intermediate_size, group_size=P, eps=config.mamba_norm_eps)
568
+ self.out_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
569
+
570
+ # ------------------------------------------------------------------ init
571
+ @torch.no_grad()
572
+ def init_ssm_parameters(self):
573
+ """Every non-Linear parameter, chosen so the mixer starts as its teacher head.
574
+
575
+ With the surgery's inheritance (C <- q_proj heads, B <- k_proj head, x <- v_proj
576
+ head, B/C_norm <- q/k_norm, out_proj <- o_proj) the block at init computes, per
577
+ head h with teacher kv group g and own rank r*:
578
+
579
+ y_h = norm( sum_s w_h(t, s) * <q_h(t), k_g(s)> * v_g(s) ) * silu(z)
580
+
581
+ i.e. the teacher's attention head with softmax replaced by a causal decay
582
+ ``w_h(t, s)`` -- a linearized copy, not a random SSM. Each line below is one
583
+ ingredient of that statement:
584
+
585
+ * ``dt_bias``: dtfix. dt ~ exp(U(log 1e-3, log 1e-1)) per head, floored at
586
+ 1e-4, stored through the softplus inverse (upstream's own dt_min/dt_max and
587
+ the values Linearization_afei/DTFIX.md re-derived for Mamba2 after Bamba's
588
+ ``fill_(1.0)`` gave dt*|A| = 21 and a 4-token memory). Horizons of 10-1000
589
+ tokens across heads at A = -1.
590
+ * ``in_proj`` rows for ``dd_dt``, ``dd_A``, ``trap``, ``angles``: **zero**. Then
591
+ dt = softplus(dt_bias) is a per-head constant, A = -heavy_tail(0) = -1 for
592
+ every head, the trapezoid weight sigmoid(0) = 1/2, and the rotation angle is
593
+ 0 -- the decay is a clean monotone distance kernel and no random RoPE
594
+ scrambles the inherited q.k geometry. All four keep a live gradient
595
+ (softplus', heavy_tail', sigmoid', sin' are nonzero at 0). The framework's
596
+ N(0, 0.02) rows would instead give every one of these an O(1) per-token
597
+ random swing at hidden 2560.
598
+ * ``B_bias``/``C_bias``: ``mamba_bc_bias_init`` (default 1.0, upstream's). It adds
599
+ ``sum(q) + sum(k) + N`` (N = 128) to every score at init, which looked like it
600
+ would swamp the inherited head; measured, the bias-1 arm beat bias 0 at every
601
+ stage-1 and 2a eval (see the config docstring), so the worry did not hold.
602
+ * ``mimo_x``: a random simplex over ranks per (h, p) -- softmax_r of N(0, 1) --
603
+ so ``sum_r v_r = x`` and the state ``sum_r k_r v_r^T = k x^T`` is *identical*
604
+ to upstream's uniform ``1/R`` at init (k is shared across ranks after
605
+ inheritance), while the per-rank values differ, which is what gives the
606
+ replicated B rows distinct gradients per rank instead of leaving the ranks
607
+ permanently tied.
608
+ * ``mimo_z``: ones (upstream): one gate for all ranks.
609
+ * ``mimo_o``: ``own_rank_matrix`` -- head h reads out the rank carrying teacher
610
+ q head h. Upstream's ``1/R`` would average the group's four heads into each.
611
+ * ``D``: ones (upstream). Under the pre-gate norm only the SSM:skip ratio
612
+ matters and the skip is small against O(|q.k|) readouts.
613
+ * ``norm.weight``: ones. Scale is re-fit by stage 1.
614
+ """
615
+ H, R, N, P, G = self.num_heads, self.mimo_rank, self.ssm_state_size, self.head_dim, self.n_groups
616
+ dev = self.dt_bias.device
617
+ dt = torch.exp(
618
+ torch.rand(H, dtype=torch.float32, device=dev)
619
+ * (math.log(self.dt_max) - math.log(self.dt_min))
620
+ + math.log(self.dt_min)
621
+ ).clamp(min=self.dt_init_floor)
622
+ inv_dt = dt + torch.log(-torch.expm1(-dt)) # softplus inverse
623
+ self.dt_bias.copy_(inv_dt.to(self.dt_bias.dtype))
624
+ self.B_bias.fill_(self.bc_bias_init)
625
+ self.C_bias.fill_(self.bc_bias_init)
626
+ self.mimo_x.copy_(
627
+ torch.softmax(torch.randn(H, R, P, dtype=torch.float32, device=dev), dim=1).to(self.mimo_x.dtype)
628
+ )
629
+ self.mimo_z.fill_(1.0)
630
+ self.mimo_o.copy_(own_rank_matrix(H, G, R).to(dev)[:, :, None].expand(H, R, P).to(self.mimo_o.dtype))
631
+ self.D.fill_(1.0)
632
+ self.norm.weight.fill_(1.0)
633
+ self.B_norm.weight.fill_(1.0)
634
+ self.C_norm.weight.fill_(1.0)
635
+ scalar_rows = 2 * self.intermediate_size + 2 * N * G * R
636
+ self.in_proj.weight[scalar_rows:].zero_()
637
+
638
+ # --------------------------------------------------------------- forward
639
+ def forward(
640
+ self,
641
+ hidden_states: torch.Tensor,
642
+ cache_params=None,
643
+ cache_position: Optional[torch.LongTensor] = None,
644
+ attention_mask: Optional[torch.Tensor] = None,
645
+ ) -> torch.Tensor:
646
+ b, l, _ = hidden_states.shape
647
+ H, R, N, P, G = self.num_heads, self.mimo_rank, self.ssm_state_size, self.head_dim, self.n_groups
648
+ dtype = hidden_states.dtype
649
+
650
+ # 2D padding mask (Bamba's apply_mask_to_padding_states): a zeroed input gives
651
+ # z = x = B = C = 0 and dd_* = 0, so a pad token writes nothing, rotates
652
+ # nothing, and only decays the state by exp(A * softplus(dt_bias)).
653
+ if attention_mask is not None and attention_mask.dim() == 2 and attention_mask.shape[1] == l:
654
+ hidden_states = hidden_states * attention_mask[:, :, None].to(dtype)
655
+
656
+ proj = self.in_proj(hidden_states)
657
+ z, x, B, C, dd_dt, dd_A, trap, angles = torch.split(proj, self.split_sizes, dim=-1)
658
+ z = z.view(b, l, H, P)
659
+ x = x.view(b, l, H, P)
660
+ B = self.B_norm(B.view(b, l, R, G, N)) # rank-major "(r g n)", as upstream
661
+ C = self.C_norm(C.view(b, l, R, G, N))
662
+
663
+ dt = F.softplus(dd_dt.float() + self.dt_bias.float()) # (b, l, H)
664
+ A = torch.clamp(-heavy_tail_activation(dd_A.float()), max=-self.a_floor) # (b, l, H)
665
+ dt_t = dt.transpose(1, 2) # (b, H, l)
666
+ adt_t = (A * dt).transpose(1, 2)
667
+ trap_t = trap.transpose(1, 2) # raw, pre-sigmoid
668
+ angles = angles.float().unsqueeze(2).expand(b, l, H, self.num_rope_angles) # raw, pre-tanh
669
+
670
+ states = None
671
+ warm = False
672
+ if cache_params is not None:
673
+ i = self.layer_idx
674
+ states = (
675
+ cache_params.angle_states[i], cache_params.ssm_states[i],
676
+ cache_params.k_states[i], cache_params.v_states[i],
677
+ )
678
+ warm = bool(cache_params.has_previous_state)
679
+
680
+ kernel = None
681
+ if x.is_cuda and not warm and not self.force_reference:
682
+ kernel = load_mimo_kernel() or None
683
+
684
+ if kernel is not None:
685
+ out = kernel(
686
+ Q=C, K=B, V=x,
687
+ ADT=adt_t, DT=dt_t, Trap=trap_t.to(dtype),
688
+ Q_bias=self.C_bias.float(), K_bias=self.B_bias.float(),
689
+ MIMO_V=self.mimo_x.float(), MIMO_Z=self.mimo_z.float(), MIMO_Out=self.mimo_o.float(),
690
+ Angles=angles, D=self.D.float(), Z=z,
691
+ chunk_size=self.chunk_size,
692
+ rotary_dim_divisor=self.rotary_dim_divisor,
693
+ dtype=dtype,
694
+ return_state=states is not None,
695
+ fuse_pregate_headwise_rms_norm=True,
696
+ outproj_norm_weight=self.norm.weight.float(),
697
+ outproj_norm_eps=self.norm.eps,
698
+ )
699
+ if states is not None:
700
+ y, angle_f, ssm_f, k_f, v_f = out
701
+ states[0].copy_(angle_f)
702
+ states[1].copy_(ssm_f)
703
+ states[2].copy_(k_f)
704
+ states[3].copy_(v_f)
705
+ else:
706
+ y = out
707
+ else:
708
+ y, finals = mamba3_mimo_reference(
709
+ C, B, x, z, adt_t, dt_t, trap_t,
710
+ self.C_bias, self.B_bias, self.mimo_x, self.mimo_z, self.mimo_o,
711
+ angles, self.D, self.norm.weight, self.norm.eps,
712
+ initial_states=states if warm else None,
713
+ )
714
+ if states is not None:
715
+ for dst, src in zip(states, finals):
716
+ dst.copy_(src.to(dst.dtype))
717
+
718
+ y = y.reshape(b, l, self.intermediate_size)
719
+ return self.out_proj(y.to(dtype))
720
+
721
+
722
+ class Qwen3Mamba3HybridCache(HybridMambaAttentionDynamicCache):
723
+ """Bamba's hybrid cache with Mamba-3's four recurrent states per converted layer.
724
+
725
+ Per converted layer (kernel ``return_state`` layout, upstream ``allocate_inference_cache``):
726
+ ``angle_states``: (B, H, A) fp32 -- cumulative rotation angle, mod 2 pi
727
+ ``ssm_states``: (B, H, P, N) fp32
728
+ ``k_states``: (B, R, H, N) model dtype -- last biased+rotated key
729
+ ``v_states``: (B, H, P) model dtype -- last raw x (pre-``mimo_x``)
730
+ ``conv_states`` are kept as ``(B, 0)`` placeholders (no conv in Mamba-3) so the
731
+ parent's ``reorder_cache`` keeps working; the three fixes from the Mamba2 cache
732
+ (int widths, ``get_seq_length`` on an unwritten cache, ``get_mask_sizes``) carry
733
+ over.
734
+ """
735
+
736
+ def __init__(self, config: Qwen3Mamba3Config, batch_size: int, dtype=torch.float16, device=None):
737
+ self.layers_block_type = config.layers_block_type
738
+ self.has_previous_state = False
739
+ H, P, N = config.mamba_n_heads, config.mamba_d_head, config.mamba_d_state
740
+ R, A = config.mamba_mimo_rank, mamba3_num_rope_angles(config)
741
+
742
+ def placeholder():
743
+ return torch.tensor([[]] * batch_size, device=device)
744
+
745
+ self.conv_states, self.ssm_states, self.transformer_layers = [], [], []
746
+ self.angle_states, self.k_states, self.v_states = [], [], []
747
+ for i in range(config.num_hidden_layers):
748
+ self.conv_states.append(placeholder())
749
+ if self.layers_block_type[i] == "mamba":
750
+ self.angle_states.append(torch.zeros(batch_size, H, A, device=device, dtype=torch.float32))
751
+ self.ssm_states.append(torch.zeros(batch_size, H, P, N, device=device, dtype=torch.float32))
752
+ self.k_states.append(torch.zeros(batch_size, R, H, N, device=device, dtype=dtype))
753
+ self.v_states.append(torch.zeros(batch_size, H, P, device=device, dtype=dtype))
754
+ else:
755
+ self.angle_states.append(placeholder())
756
+ self.ssm_states.append(placeholder())
757
+ self.k_states.append(placeholder())
758
+ self.v_states.append(placeholder())
759
+ self.transformer_layers.append(i)
760
+ self.key_cache = [placeholder() for _ in range(config.num_hidden_layers)]
761
+ self.value_cache = [placeholder() for _ in range(config.num_hidden_layers)]
762
+
763
+ def reorder_cache(self, beam_idx: torch.LongTensor):
764
+ super().reorder_cache(beam_idx)
765
+ for states in (self.angle_states, self.k_states, self.v_states):
766
+ for i in range(len(states)):
767
+ states[i] = states[i].index_select(0, beam_idx.to(states[i].device))
768
+
769
+ def get_seq_length(self, layer_idx: Optional[int] = 0) -> int:
770
+ if layer_idx not in self.transformer_layers:
771
+ layer_idx = self.transformer_layers[0]
772
+ if len(self.key_cache) <= layer_idx or self.key_cache[layer_idx].numel() == 0:
773
+ return 0
774
+ return self.key_cache[layer_idx].shape[-2]
775
+
776
+ def get_mask_sizes(self, cache_position: torch.Tensor, layer_idx: int) -> tuple[int, int]:
777
+ query_length = cache_position.shape[0]
778
+ return self.get_seq_length(layer_idx) + query_length, 0
779
+
780
+
781
+ class Qwen3Mamba3LinearDecoderLayer(GradientCheckpointingLayer):
782
+ """Pre-norm decoder layer with the Mamba-3 MIMO mixer under ``linear_attn``."""
783
+
784
+ def __init__(self, config: Qwen3Mamba3Config, layer_idx: int):
785
+ super().__init__()
786
+ self.hidden_size = config.hidden_size
787
+ self.layer_type = self.attention_type = "linear_attention"
788
+ self.linear_attn = Qwen3Mamba3Mixer(config, layer_idx)
789
+ self.mlp = Qwen3MLP(config)
790
+ self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
791
+ self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
792
+
793
+ def forward(
794
+ self,
795
+ hidden_states: torch.Tensor,
796
+ attention_mask: Optional[torch.Tensor] = None,
797
+ position_ids: Optional[torch.LongTensor] = None,
798
+ past_key_values=None,
799
+ use_cache: Optional[bool] = False,
800
+ cache_position: Optional[torch.LongTensor] = None,
801
+ position_embeddings=None, # unused: the mixer's rotation is data-dependent
802
+ **kwargs,
803
+ ) -> torch.Tensor:
804
+ residual = hidden_states
805
+ hidden_states = self.input_layernorm(hidden_states)
806
+ hidden_states = self.linear_attn(
807
+ hidden_states=hidden_states,
808
+ cache_params=past_key_values,
809
+ cache_position=cache_position,
810
+ attention_mask=attention_mask,
811
+ )
812
+ hidden_states = residual + hidden_states
813
+
814
+ residual = hidden_states
815
+ hidden_states = self.post_attention_layernorm(hidden_states)
816
+ hidden_states = self.mlp(hidden_states)
817
+ return residual + hidden_states
818
+
819
+
820
+ class Qwen3Mamba3PreTrainedModel(PreTrainedModel):
821
+ config_class = Qwen3Mamba3Config
822
+ base_model_prefix = "model"
823
+ supports_gradient_checkpointing = True
824
+ _no_split_modules = ["Qwen3DecoderLayer", "Qwen3Mamba3LinearDecoderLayer"]
825
+ _skip_keys_device_placement = "past_key_values"
826
+ _supports_flash_attn_2 = True
827
+ _supports_sdpa = True
828
+ _is_stateful = True
829
+
830
+ def _init_weights(self, module):
831
+ # Generic init first (Linear N(0, 0.02), *RMSNorm* weights -> 1), then the
832
+ # mixer's own parameters. ``nn.Module.apply`` visits children before the
833
+ # parent, so ``in_proj`` is already drawn when the mixer zeroes its scalar rows.
834
+ super()._init_weights(module)
835
+ if isinstance(module, Qwen3Mamba3Mixer):
836
+ module.init_ssm_parameters()
837
+
838
+
839
+ class Qwen3Mamba3Model(Qwen3Mamba3PreTrainedModel):
840
+ def __init__(self, config: Qwen3Mamba3Config):
841
+ super().__init__(config)
842
+ assert config.layer_types is not None and len(config.layer_types) == config.num_hidden_layers
843
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, config.pad_token_id)
844
+ self.layers = nn.ModuleList(
845
+ [
846
+ Qwen3DecoderLayer(config, i)
847
+ if config.layer_types[i] == "full_attention"
848
+ else Qwen3Mamba3LinearDecoderLayer(config, i)
849
+ for i in range(config.num_hidden_layers)
850
+ ]
851
+ )
852
+ self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
853
+ self.rotary_emb = Qwen3RotaryEmbedding(config=config)
854
+ self.gradient_checkpointing = False
855
+ self.post_init()
856
+
857
+ def forward(
858
+ self,
859
+ input_ids: Optional[torch.LongTensor] = None,
860
+ attention_mask: Optional[torch.Tensor] = None,
861
+ position_ids: Optional[torch.LongTensor] = None,
862
+ past_key_values=None,
863
+ inputs_embeds: Optional[torch.FloatTensor] = None,
864
+ use_cache: Optional[bool] = None,
865
+ cache_position: Optional[torch.LongTensor] = None,
866
+ **kwargs,
867
+ ) -> BaseModelOutputWithPast:
868
+ if (input_ids is None) ^ (inputs_embeds is not None):
869
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
870
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
871
+ if self.gradient_checkpointing and self.training:
872
+ use_cache = False
873
+
874
+ if inputs_embeds is None:
875
+ inputs_embeds = self.embed_tokens(input_ids)
876
+
877
+ if use_cache and past_key_values is None:
878
+ past_key_values = Qwen3Mamba3HybridCache(
879
+ config=self.config,
880
+ batch_size=inputs_embeds.shape[0],
881
+ dtype=inputs_embeds.dtype,
882
+ device=inputs_embeds.device,
883
+ )
884
+
885
+ if cache_position is None:
886
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
887
+ cache_position = torch.arange(
888
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1],
889
+ device=inputs_embeds.device,
890
+ )
891
+ if position_ids is None:
892
+ position_ids = cache_position.unsqueeze(0)
893
+
894
+ causal_mask = create_causal_mask(
895
+ config=self.config,
896
+ input_embeds=inputs_embeds,
897
+ attention_mask=attention_mask,
898
+ cache_position=cache_position,
899
+ past_key_values=past_key_values,
900
+ position_ids=position_ids,
901
+ )
902
+ linear_attn_mask = self._update_linear_attn_mask(attention_mask, cache_position)
903
+
904
+ hidden_states = inputs_embeds
905
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
906
+
907
+ for decoder_layer in self.layers:
908
+ layer_mask = (
909
+ linear_attn_mask if decoder_layer.attention_type == "linear_attention" else causal_mask
910
+ )
911
+ hidden_states = decoder_layer(
912
+ hidden_states,
913
+ attention_mask=layer_mask,
914
+ position_ids=position_ids,
915
+ past_key_values=past_key_values if use_cache else None,
916
+ use_cache=use_cache,
917
+ cache_position=cache_position,
918
+ position_embeddings=position_embeddings,
919
+ **kwargs,
920
+ )
921
+
922
+ # The mixer takes the recurrent (warm) path only once this flag is up; the
923
+ # model raises it after the first cached forward (same contract as Bamba).
924
+ if use_cache and past_key_values is not None and not past_key_values.has_previous_state:
925
+ past_key_values.has_previous_state = True
926
+
927
+ hidden_states = self.norm(hidden_states)
928
+ return BaseModelOutputWithPast(
929
+ last_hidden_state=hidden_states,
930
+ past_key_values=past_key_values if use_cache else None,
931
+ )
932
+
933
+ def _update_linear_attn_mask(self, attention_mask, cache_position):
934
+ linear_attn_mask = attention_mask
935
+ if cache_position[0] > 0 or (attention_mask is not None and torch.all(attention_mask == 1)):
936
+ linear_attn_mask = None
937
+ return linear_attn_mask
938
+
939
+
940
+ class Qwen3Mamba3ForCausalLM(Qwen3Mamba3PreTrainedModel, GenerationMixin):
941
+ _tied_weights_keys = ["lm_head.weight"]
942
+
943
+ def __init__(self, config: Qwen3Mamba3Config):
944
+ super().__init__(config)
945
+ self.model = Qwen3Mamba3Model(config)
946
+ self.vocab_size = config.vocab_size
947
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
948
+ self.post_init()
949
+
950
+ def forward(
951
+ self,
952
+ input_ids: Optional[torch.LongTensor] = None,
953
+ attention_mask: Optional[torch.Tensor] = None,
954
+ position_ids: Optional[torch.LongTensor] = None,
955
+ past_key_values=None,
956
+ inputs_embeds: Optional[torch.FloatTensor] = None,
957
+ labels: Optional[torch.LongTensor] = None,
958
+ use_cache: Optional[bool] = None,
959
+ cache_position: Optional[torch.LongTensor] = None,
960
+ logits_to_keep=0,
961
+ **kwargs,
962
+ ) -> CausalLMOutputWithPast:
963
+ outputs = self.model(
964
+ input_ids=input_ids,
965
+ attention_mask=attention_mask,
966
+ position_ids=position_ids,
967
+ past_key_values=past_key_values,
968
+ inputs_embeds=inputs_embeds,
969
+ use_cache=use_cache,
970
+ cache_position=cache_position,
971
+ **kwargs,
972
+ )
973
+ hidden_states = outputs.last_hidden_state
974
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
975
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
976
+
977
+ loss = None
978
+ if labels is not None:
979
+ loss = self.loss_function(logits, labels, self.vocab_size, **kwargs)
980
+
981
+ return CausalLMOutputWithPast(
982
+ loss=loss,
983
+ logits=logits,
984
+ past_key_values=outputs.past_key_values,
985
+ )
986
+
987
+
988
+ def register():
989
+ """Make qwen3_mamba3 checkpoints loadable through the Auto* classes."""
990
+ from transformers import AutoConfig, AutoModelForCausalLM
991
+
992
+ try:
993
+ AutoConfig.register("qwen3_mamba3", Qwen3Mamba3Config)
994
+ except ValueError:
995
+ pass # already registered in this process
996
+ AutoModelForCausalLM.register(Qwen3Mamba3Config, Qwen3Mamba3ForCausalLM, exist_ok=True)
997
+
998
+
999
+ try: # convenience for direct imports; auto_map already covers the
1000
+ register() # remote-code path, so a registry collision is not fatal here
1001
+ except Exception: # pragma: no cover
1002
+ pass