mrs83 commited on
Commit
deb8a77
Β·
verified Β·
1 Parent(s): 652aabe

Upload 3 files

Browse files
Files changed (3) hide show
  1. configuration_echo.py +101 -0
  2. modeling_echo.py +1458 -0
  3. triton_scan.py +521 -0
configuration_echo.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from transformers import PretrainedConfig
4
+
5
+
6
+ class EchoConfig(PretrainedConfig):
7
+ model_type = "echo"
8
+
9
+ def __init__(
10
+ self,
11
+ vocab_size=49152,
12
+ embed_dim=None,
13
+ num_layers=4,
14
+ num_heads=4,
15
+ mlp_ratio=4,
16
+ gate_bias_init=0.0,
17
+ use_hybrid_attention=True,
18
+ use_rmsnorm=True,
19
+ mlp_bias: bool = False,
20
+ # --- Classification fields (optional, ignored by CausalLM) ---
21
+ num_labels: int = 2,
22
+ id2label: Optional[dict] = None,
23
+ label2id: Optional[dict] = None,
24
+ classifier_dropout: float = 0.0,
25
+ **kwargs,
26
+ ):
27
+ # Synchronize hidden_size / embed_dim (HF synonym pair).
28
+ # Priority: explicit embed_dim > explicit hidden_size > package default (768).
29
+ hidden_size = kwargs.pop("hidden_size", None)
30
+
31
+ if embed_dim is None and hidden_size is None:
32
+ embed_dim = 768 # package default
33
+ elif embed_dim is None:
34
+ embed_dim = hidden_size
35
+ elif hidden_size is None:
36
+ hidden_size = embed_dim
37
+ elif embed_dim != hidden_size:
38
+ raise ValueError(
39
+ f"embed_dim ({embed_dim}) and hidden_size ({hidden_size}) must be equal in "
40
+ "Echo-DSRN β€” they are the same architectural dimension. Pass only one."
41
+ )
42
+
43
+ hidden_size = embed_dim # keep them in sync
44
+
45
+ self.vocab_size = vocab_size
46
+ self.embed_dim = embed_dim
47
+ self.hidden_size = hidden_size
48
+ self.num_layers = num_layers
49
+ self.num_heads = num_heads
50
+ self.mlp_ratio = mlp_ratio
51
+ self.gate_bias_init = gate_bias_init
52
+ self.use_hybrid_attention = use_hybrid_attention
53
+ self.use_rmsnorm = use_rmsnorm
54
+ self.mlp_bias = mlp_bias
55
+ self.classifier_dropout = classifier_dropout
56
+
57
+ # Standard HF aliases
58
+ self.num_hidden_layers = num_layers
59
+ self.num_attention_heads = num_heads
60
+
61
+ # TGI/HF AutoMap support
62
+ self.auto_map = {
63
+ "AutoConfig": "configuration_echo.EchoConfig",
64
+ "AutoModel": "modeling_echo.EchoModel",
65
+ "AutoModelForCausalLM": "modeling_echo.EchoForCausalLM",
66
+ "AutoModelForSequenceClassification": ("modeling_echo.EchoForSequenceClassification"),
67
+ }
68
+
69
+ # vLLM Advanced Parallelism Plans
70
+ self.base_model_tp_plan = {
71
+ "model.embedding": "rowwise",
72
+ "lm_head": "colwise",
73
+ "model.blocks.*.attn.qkv_proj": "colwise",
74
+ "model.blocks.*.attn.out_proj": "rowwise",
75
+ "model.blocks.*.mlp_up": "colwise",
76
+ "model.blocks.*.mlp_down": "rowwise",
77
+ "model.blocks.*.linear_gate": "colwise",
78
+ "model.blocks.*.linear_memory": "colwise",
79
+ "model.blocks.*.linear_read": "rowwise",
80
+ }
81
+
82
+ self.base_model_pp_plan = {
83
+ "blocks": (["x", "state_prev"], ["x", "h_new_full"]) # Inputs # Outputs
84
+ }
85
+
86
+ # PretrainedConfig manages id2label / label2id / num_labels as
87
+ # properties internally. Pass them through super().__init__ so HF's
88
+ # property setters run in the correct order. We must NOT pop them here.
89
+ if id2label is not None:
90
+ kwargs["id2label"] = {int(k): v for k, v in id2label.items()}
91
+ kwargs["label2id"] = {v: int(k) for k, v in id2label.items()}
92
+ elif "id2label" not in kwargs:
93
+ # Inject defaults so the property chain initialises cleanly
94
+ default_id2label = {i: str(i) for i in range(num_labels)}
95
+ kwargs["id2label"] = default_id2label
96
+ kwargs["label2id"] = {v: k for k, v in default_id2label.items()}
97
+
98
+ if label2id is not None and "label2id" not in kwargs:
99
+ kwargs["label2id"] = label2id
100
+
101
+ super().__init__(**kwargs)
modeling_echo.py ADDED
@@ -0,0 +1,1458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Union
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from transformers import GenerationMixin, PreTrainedModel
7
+ from transformers.modeling_outputs import (
8
+ BaseModelOutputWithPast,
9
+ CausalLMOutputWithPast,
10
+ SequenceClassifierOutputWithPast,
11
+ )
12
+
13
+ from .configuration_echo import EchoConfig
14
+
15
+ if TYPE_CHECKING:
16
+ # Force HF trust_remote_code AST parser to bundle triton_scan.py
17
+ pass
18
+
19
+ try:
20
+ # pyrefly: ignore [missing-import]
21
+ from vllm.model_executor.models.transformers import ALL_ATTENTION_FUNCTIONS
22
+ except ImportError:
23
+ ALL_ATTENTION_FUNCTIONS = {}
24
+
25
+ try:
26
+ from transformers.cache_utils import Cache
27
+ except ImportError:
28
+
29
+ class Cache:
30
+ pass
31
+
32
+
33
+ class EchoCache(Cache):
34
+ """
35
+ Custom Cache to prevent Hugging Face's DynamicCache from dropping
36
+ the (k_attn, v_attn) elements from the DSRN 4-tuple state.
37
+ """
38
+
39
+ def __init__(self, states=None):
40
+ self.states = states if states is not None else []
41
+ self.layers = self.states # HF expectation
42
+
43
+ @property
44
+ def is_compileable(self):
45
+ return False
46
+
47
+ def get_seq_length(self, layer_idx=0):
48
+ if not self.states or len(self.states) <= layer_idx:
49
+ return 0
50
+ state = self.states[layer_idx]
51
+ if len(state) == 4:
52
+ return state[2].shape[2]
53
+ return 0
54
+
55
+ def get_max_length(self):
56
+ return None
57
+
58
+ def update(
59
+ self,
60
+ key_states: torch.Tensor,
61
+ value_states: torch.Tensor,
62
+ layer_idx: int,
63
+ cache_kwargs: Optional[dict] = None,
64
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
65
+ # EchoModel handles its own cache updates internally within the blocks.
66
+ # This update method is just a shim to satisfy the Cache protocol.
67
+ # k, v are already updated in the state tuple returned by the block.
68
+ if len(self.states) > layer_idx:
69
+ state = self.states[layer_idx]
70
+ if len(state) == 4:
71
+ return state[2], state[3]
72
+ return key_states, value_states
73
+
74
+ def get_usable_length(self, new_seq_length, layer_idx=0):
75
+ return self.get_seq_length(layer_idx)
76
+
77
+ def __getitem__(self, idx):
78
+ return self.states[idx]
79
+
80
+ def __len__(self):
81
+ return len(self.states)
82
+
83
+ def __iter__(self):
84
+ return iter(self.states)
85
+
86
+ def reorder_cache(self, beam_idx: torch.LongTensor):
87
+ reordered_states = []
88
+ for layer_state in self.states:
89
+ reordered_layer_state = tuple(
90
+ tensor.index_select(0, beam_idx.to(tensor.device)) for tensor in layer_state
91
+ )
92
+ reordered_states.append(reordered_layer_state)
93
+ self.states = reordered_states
94
+
95
+
96
+ # --- STANDALONE KERNELS (AUTOMAGICALLY INLINED) ---
97
+ def _sequential_scan(a, b, h):
98
+ """
99
+ Core sequential scan for a batch of sequences.
100
+ Vectorized across all dimensions except time.
101
+ """
102
+ a.shape[:-1]
103
+ a.shape[-1]
104
+ # a, b: (..., T, D)
105
+ # h: (..., D)
106
+ T = a.shape[-2]
107
+
108
+ res = torch.empty_like(b)
109
+ curr_h = h
110
+ for t in range(T):
111
+ curr_h = a[..., t, :] * curr_h + b[..., t, :]
112
+ res[..., t, :] = curr_h
113
+ return res, curr_h
114
+
115
+
116
+ def dsrn_parallel_scan(g_t, m_t, c_0=None, chunk_size=32, use_triton=False):
117
+ """
118
+ Parallel implementation of the DSRN slow-state update:
119
+ c_t = (1 - g_t) * c_{t-1} + g_t * m_t
120
+
121
+ Uses a Hierarchical Chunked Scan for O(T/K + K) speed and stability,
122
+ or a custom Triton kernel for dramatically reduced memory bandwidth.
123
+ """
124
+ # Global Override: Disabling Triton scan while debugging LoRA NaN gradients
125
+ if use_triton and g_t.is_cuda:
126
+ try:
127
+ from .triton_scan import triton_dsrn_parallel_scan
128
+
129
+ return triton_dsrn_parallel_scan(g_t, m_t, c_0)
130
+ except ImportError:
131
+ import warnings
132
+
133
+ warnings.warn("Triton scan unavailable. Falling back to PyTorch scan.", UserWarning)
134
+
135
+ orig_dtype = g_t.dtype
136
+ a = (1.0 - g_t).float()
137
+ b = (g_t * m_t).float()
138
+
139
+ B, T, D = a.shape
140
+ device = a.device
141
+
142
+ # Pad T to be multiple of chunk_size
143
+ pad_len = (chunk_size - (T % chunk_size)) % chunk_size
144
+ if pad_len > 0:
145
+ a = F.pad(a, (0, 0, 0, pad_len), value=1.0)
146
+ b = F.pad(b, (0, 0, 0, pad_len), value=0.0)
147
+
148
+ new_T = T + pad_len
149
+ num_chunks = new_T // chunk_size
150
+
151
+ # 1. Reshape to (B, num_chunks, chunk_size, D)
152
+ a_chunks = a.view(B, num_chunks, chunk_size, D)
153
+ b_chunks = b.view(B, num_chunks, chunk_size, D)
154
+
155
+ # 2. Local scan within each chunk (vectorized across B and num_chunks)
156
+ h_init_local = torch.zeros(B, num_chunks, D, device=device, dtype=torch.float32)
157
+ c_res, c_final = _sequential_scan(a_chunks, b_chunks, h_init_local)
158
+
159
+ # Summary of a for each chunk (product of a)
160
+ a_final = torch.prod(a_chunks, dim=2) # (B, num_chunks, D)
161
+
162
+ # 3. Global scan across chunk summaries
163
+ h_0 = c_0.float() if c_0 is not None else torch.zeros(B, D, device=device, dtype=torch.float32)
164
+
165
+ # h_chunk_outputs[:, j] is the state AFTER chunk j.
166
+ h_chunk_outputs, _ = _sequential_scan(a_final, c_final, h_0)
167
+ # The state BEFORE chunk j is h_chunk_outputs[:, j-1].
168
+ h_starts = torch.cat([h_0.unsqueeze(1), h_chunk_outputs[:, :-1]], dim=1)
169
+
170
+ # 4. Final combine: h_{j, i} = a_prefix_{j, i} * h_starts[j] + c_res[j, i]
171
+ a_prefix = torch.cumprod(a_chunks, dim=2)
172
+ final_h = a_prefix * h_starts.unsqueeze(2) + c_res
173
+
174
+ # Reshape back and crop, then cast back to original dtype
175
+ return final_h.view(B, -1, D)[:, :T].to(orig_dtype)
176
+
177
+
178
+ def rms_norm_fn(hidden_states, weight, eps=1e-6):
179
+ input_dtype = hidden_states.dtype
180
+ hidden_states = hidden_states.contiguous().to(torch.float32)
181
+ variance = (hidden_states * hidden_states).mean(-1, keepdim=True)
182
+ hidden_states = hidden_states * torch.rsqrt(variance + eps)
183
+ return weight * hidden_states.to(input_dtype)
184
+
185
+
186
+ def dsrn_parallel_kernel_legacy(
187
+ model_block: nn.Module,
188
+ x: torch.Tensor,
189
+ h_prev: torch.Tensor,
190
+ c_prev: torch.Tensor,
191
+ eos_mask: Optional[torch.Tensor] = None,
192
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
193
+ """
194
+ Legacy DSRN kernel (Fixed LayerNorm, No Surprise Read).
195
+ Identical to the version that passed verification.
196
+ """
197
+ B, T, D = x.shape
198
+
199
+ # 1. Norm and Projections
200
+ x_norm = F.layer_norm(
201
+ x,
202
+ (D,),
203
+ weight=model_block.norm_fast.weight,
204
+ bias=model_block.norm_fast.bias,
205
+ )
206
+
207
+ # Fast State Path (Scan)
208
+ gru_proj = F.linear(x_norm, model_block.gru_cell.weight_ih, model_block.gru_cell.bias_ih)
209
+ z_all = torch.sigmoid(gru_proj[:, :, :D])
210
+ r_all = torch.tanh(gru_proj[:, :, 2 * D :]) # Optimization: slice instead of chunk
211
+
212
+ # --- EOS RESET LOGIC (Fast State) ---
213
+ if eos_mask is not None:
214
+ reset_mask = torch.roll(eos_mask, shifts=1, dims=1)
215
+ reset_mask[:, 0] = (
216
+ 0 # First token reset depends on previous chunk eos, handled by h_prev/c_prev passing 0
217
+ )
218
+
219
+ # Apply strict reset to z_all
220
+ z_all = torch.where(reset_mask.unsqueeze(-1) > 0, torch.ones_like(z_all), z_all)
221
+
222
+ # h_t = (1 - z_t) * h_{t-1} + z_t * r_t
223
+ h_all = dsrn_parallel_scan(
224
+ z_all, r_all, h_prev, use_triton=getattr(model_block, "use_triton", False)
225
+ )
226
+ h_new = h_all[:, -1]
227
+
228
+ # 2. Slow State Path
229
+ # CAUSAL SHIFT: Predict x[t] using h[t-1]
230
+ # h_all is [h_1, ..., h_T]. We need [h_0, ..., h_{T-1}]
231
+ # Prepend h_prev to shift
232
+ h_shifted = torch.cat([h_prev.unsqueeze(1), h_all[:, :-1, :]], dim=1)
233
+
234
+ x_pred = model_block.linear_pred(h_shifted)
235
+ diff = x - x_pred
236
+ error = torch.clamp(diff * diff, max=10.0).mean(dim=-1, keepdim=True)
237
+ # Constrain surprise_lambda strictly positive to guarantee error opens the memory gate
238
+ surprise_signal = error * torch.nn.functional.softplus(model_block.surprise_lambda)
239
+
240
+ # Gates
241
+ gate_logits = model_block.linear_gate(h_all) + surprise_signal
242
+ g_all = torch.sigmoid(gate_logits)
243
+ m_all = torch.tanh(model_block.linear_memory(h_all))
244
+
245
+ # --- EOS RESET LOGIC (Slow State) ---
246
+ if eos_mask is not None:
247
+ reset_mask = torch.roll(eos_mask, shifts=1, dims=1)
248
+ reset_mask[:, 0] = 0
249
+
250
+ g_all = torch.where(reset_mask.unsqueeze(-1) > 0, torch.zeros_like(g_all), g_all)
251
+
252
+ # c_t
253
+ c_all = dsrn_parallel_scan(
254
+ g_all, m_all, c_prev, use_triton=getattr(model_block, "use_triton", False)
255
+ )
256
+ c_new = c_all[:, -1]
257
+
258
+ # --- Inter-Chunk Reset ---
259
+ # If the LAST token is EOS, then h_new/c_new (which are states FOR NEXT CHUNK) must be 0.
260
+ if eos_mask is not None:
261
+ last_is_eos = eos_mask[:, -1].float() # (B,)
262
+ keep_prob = (1.0 - last_is_eos).unsqueeze(-1) # (B, 1)
263
+ h_new = h_new * keep_prob
264
+ c_new = c_new * keep_prob
265
+ gate_stats = g_all.mean(dim=-1)
266
+
267
+ # 3. Final MLP Path
268
+ h_norm = F.layer_norm(
269
+ h_all, (D,), weight=model_block.norm_ff.weight, bias=model_block.norm_ff.bias
270
+ )
271
+ mlp_out = model_block.mlp_down(model_block.mlp_act(model_block.mlp_up(h_norm)))
272
+
273
+ x_out = x + mlp_out
274
+
275
+ # Continuous Read (Surprise Gate Fix)
276
+ # Enabled on Legacy to fix Disconnected Slow State bug while keeping LayerNorm
277
+ x_out = x_out + model_block.linear_read(c_all)
278
+
279
+ return x_out, h_new, c_new, gate_stats
280
+
281
+
282
+ def dsrn_parallel_kernel_hybrid(
283
+ model_block: nn.Module,
284
+ x: torch.Tensor,
285
+ h_prev: torch.Tensor,
286
+ c_prev: torch.Tensor,
287
+ eos_mask: Optional[torch.Tensor] = None,
288
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
289
+ """
290
+ Hybrid DSRN kernel (RMSNorm + Surprise Read).
291
+ """
292
+ B, T, D = x.shape
293
+
294
+ # 1. Norm (RMSNorm hardcoded for Hybrid path)
295
+ x_norm = rms_norm_fn(x, model_block.norm_fast.weight)
296
+
297
+ # Fast State
298
+ gru_proj = F.linear(x_norm, model_block.gru_cell.weight_ih, model_block.gru_cell.bias_ih)
299
+ z_all = torch.sigmoid(gru_proj[:, :, :D])
300
+ r_all = torch.tanh(gru_proj[:, :, 2 * D :])
301
+
302
+ # --- EOS RESET LOGIC (Fast State) ---
303
+ if eos_mask is not None:
304
+ reset_mask = torch.roll(eos_mask, shifts=1, dims=1)
305
+ reset_mask[:, 0] = 0
306
+ z_all = torch.where(reset_mask.unsqueeze(-1) > 0, torch.ones_like(z_all), z_all)
307
+
308
+ h_all = dsrn_parallel_scan(
309
+ z_all, r_all, h_prev, use_triton=getattr(model_block, "use_triton", False)
310
+ )
311
+ h_new = h_all[:, -1]
312
+
313
+ # 2. Slow State
314
+ # CAUSAL SHIFT: Predict x[t] using h[t-1]
315
+ h_shifted = torch.cat([h_prev.unsqueeze(1), h_all[:, :-1, :]], dim=1)
316
+
317
+ x_pred = model_block.linear_pred(h_shifted)
318
+ diff = x - x_pred
319
+ error = torch.clamp(diff * diff, max=10.0).mean(dim=-1, keepdim=True)
320
+ # Constrain surprise_lambda strictly positive to guarantee error opens the memory gate
321
+ surprise_signal = error * torch.nn.functional.softplus(model_block.surprise_lambda)
322
+
323
+ gate_logits = model_block.linear_gate(h_all) + surprise_signal
324
+ g_all = torch.sigmoid(gate_logits)
325
+ m_all = torch.tanh(model_block.linear_memory(h_all))
326
+
327
+ # --- EOS RESET LOGIC (Slow State) ---
328
+ if eos_mask is not None:
329
+ reset_mask = torch.roll(eos_mask, shifts=1, dims=1)
330
+ reset_mask[:, 0] = 0
331
+ g_all = torch.where(reset_mask.unsqueeze(-1) > 0, torch.zeros_like(g_all), g_all)
332
+
333
+ c_all = dsrn_parallel_scan(
334
+ g_all, m_all, c_prev, use_triton=getattr(model_block, "use_triton", False)
335
+ )
336
+ c_new = c_all[:, -1]
337
+
338
+ # --- Inter-Chunk Reset ---
339
+ if eos_mask is not None:
340
+ last_is_eos = eos_mask[:, -1].float()
341
+ keep_prob = (1.0 - last_is_eos).unsqueeze(-1)
342
+ h_new = h_new * keep_prob
343
+ c_new = c_new * keep_prob
344
+ gate_stats = g_all.mean(dim=-1)
345
+
346
+ # 3. Final MLP
347
+ h_norm = rms_norm_fn(h_all, model_block.norm_ff.weight)
348
+ mlp_out = model_block.mlp_down(model_block.mlp_act(model_block.mlp_up(h_norm)))
349
+ x_out = x + mlp_out
350
+
351
+ # Continuous Read (Hybrid Feature)
352
+ if model_block.use_hybrid_attention:
353
+ x_out = x_out + model_block.linear_read(c_all)
354
+
355
+ return x_out, h_new, c_new, gate_stats
356
+
357
+
358
+ def dsrn_parallel_kernel(
359
+ model_block: nn.Module,
360
+ x: torch.Tensor,
361
+ h_prev: torch.Tensor,
362
+ c_prev: torch.Tensor,
363
+ eos_mask: Optional[torch.Tensor] = None,
364
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
365
+ """
366
+ Wrapper for backward compatibility. Dispatches based on config.
367
+ """
368
+ if getattr(model_block, "use_rmsnorm", False):
369
+ return dsrn_parallel_kernel_hybrid(model_block, x, h_prev, c_prev, eos_mask=eos_mask)
370
+ return dsrn_parallel_kernel_legacy(model_block, x, h_prev, c_prev, eos_mask=eos_mask)
371
+
372
+
373
+ class HymbaRMSNorm(nn.Module):
374
+ def __init__(self, hidden_size, eps=1e-6):
375
+ """
376
+ HymbaRMSNorm is equivalent to T5LayerNorm
377
+ """
378
+ super().__init__()
379
+ self.weight = nn.Parameter(torch.ones(hidden_size))
380
+ self.variance_epsilon = eps
381
+
382
+ def forward(self, hidden_states):
383
+ input_dtype = hidden_states.dtype
384
+ hidden_states = hidden_states.to(torch.float32)
385
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
386
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
387
+ return self.weight * hidden_states.to(input_dtype)
388
+
389
+
390
+ class EchoRotaryEmbedding(nn.Module):
391
+ def __init__(self, dim, max_position_embeddings=4096, base=10000.0, device=None):
392
+ super().__init__()
393
+ self.dim = dim
394
+ self.max_position_embeddings = max_position_embeddings
395
+ self.base = base
396
+ self.device = device
397
+
398
+ # We NO LONGER use buffers here because they are being corrupted by
399
+ # Hugging Face's weight loading mechanism for this specific model.
400
+ # We will compute and move them on the first forward pass.
401
+ self._cos_cached = None
402
+ self._sin_cached = None
403
+
404
+ def _set_cos_sin_cache(self, seq_len, device, dtype):
405
+ self.max_seq_len_cached = seq_len
406
+ # Compute inv_freq locally
407
+ inv_freq = 1.0 / (
408
+ self.base
409
+ ** (torch.arange(0, self.dim, 2, dtype=torch.float32, device=device) / self.dim)
410
+ )
411
+ t = torch.arange(self.max_seq_len_cached, device=device, dtype=torch.float32)
412
+ freqs = torch.einsum("i,j->ij", t, inv_freq)
413
+ emb = torch.cat((freqs, freqs), dim=-1)
414
+
415
+ self._cos_cached = emb.cos().to(dtype)
416
+ self._sin_cached = emb.sin().to(dtype)
417
+
418
+ def forward(self, x, seq_len=None):
419
+ if (
420
+ self._cos_cached is None
421
+ or seq_len > self.max_seq_len_cached
422
+ or self._cos_cached.device != x.device
423
+ ):
424
+ self._set_cos_sin_cache(
425
+ seq_len=max(seq_len, self.max_position_embeddings), device=x.device, dtype=x.dtype
426
+ )
427
+
428
+ return (
429
+ self._cos_cached[:seq_len].to(dtype=x.dtype),
430
+ self._sin_cached[:seq_len].to(dtype=x.dtype),
431
+ )
432
+
433
+
434
+ def rotate_half(x):
435
+ """Rotates half the hidden dims of the input."""
436
+ x1 = x[..., : x.shape[-1] // 2]
437
+ x2 = x[..., x.shape[-1] // 2 :]
438
+ return torch.cat((-x2, x1), dim=-1)
439
+
440
+
441
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids, unsqueeze_dim=1):
442
+ cos = cos[position_ids].unsqueeze(unsqueeze_dim) # (B, 1, T, D)
443
+ sin = sin[position_ids].unsqueeze(unsqueeze_dim) # (B, 1, T, D)
444
+ q_embed = (q * cos) + (rotate_half(q) * sin)
445
+ k_embed = (k * cos) + (rotate_half(k) * sin)
446
+ return q_embed, k_embed
447
+
448
+
449
+ class SlidingWindowAttention(nn.Module):
450
+ def __init__(self, config: EchoConfig):
451
+ super().__init__()
452
+ self.hidden_size = config.hidden_size
453
+ self.num_heads = config.num_heads
454
+ self.head_dim = self.hidden_size // self.num_heads
455
+ self.window_size = getattr(config, "window_size", 128)
456
+
457
+ self.qkv_proj = nn.Linear(self.hidden_size, 3 * self.hidden_size, bias=False)
458
+ self.out_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
459
+
460
+ self.rotary_emb = EchoRotaryEmbedding(
461
+ self.head_dim,
462
+ base=getattr(config, "rope_theta", 10000.0),
463
+ )
464
+
465
+ def forward(
466
+ self,
467
+ x,
468
+ past_key_values: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
469
+ position_ids: Optional[torch.LongTensor] = None,
470
+ **kwargs,
471
+ ):
472
+ B, T, C = x.shape
473
+ qkv = self.qkv_proj(x)
474
+ q, k, v = qkv.chunk(3, dim=-1)
475
+
476
+ # Reshape for multi-head attention
477
+ q = q.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
478
+ k = k.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
479
+ v = v.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
480
+
481
+ # --- RoPE Injection ---
482
+ if position_ids is None:
483
+ # Fallback if position_ids was not passed
484
+ seq_length_with_past = T
485
+ if past_key_values is not None:
486
+ seq_length_with_past += past_key_values[0].shape[2]
487
+ position_ids = (
488
+ torch.arange(
489
+ seq_length_with_past - T,
490
+ seq_length_with_past,
491
+ dtype=torch.long,
492
+ device=x.device,
493
+ )
494
+ .unsqueeze(0)
495
+ .view(-1, T)
496
+ )
497
+
498
+ kv_seq_len = k.shape[2]
499
+ if past_key_values is not None:
500
+ kv_seq_len += past_key_values[0].shape[2]
501
+
502
+ cos, sin = self.rotary_emb(v, seq_len=kv_seq_len)
503
+ q, k = apply_rotary_pos_emb(q, k, cos, sin, position_ids)
504
+ # ----------------------
505
+
506
+ if past_key_values is not None:
507
+ k_past, v_past = past_key_values
508
+ k = torch.cat([k_past, k], dim=2)
509
+ v = torch.cat([v_past, v], dim=2)
510
+
511
+ # The cache MUST store the full history, do not overwrite it with truncated slices
512
+ current_key_value = (k, v)
513
+
514
+ # Create slices for attention computation
515
+ k_attn = k
516
+ v_attn = v
517
+
518
+ # Enforce Sliding Window (Truncate oldest tokens for attention ONLY)
519
+ if self.window_size is not None and k_attn.shape[2] > self.window_size:
520
+ k_attn = k_attn[:, :, -self.window_size :, :]
521
+ v_attn = v_attn[:, :, -self.window_size :, :]
522
+
523
+ attn_fn = ALL_ATTENTION_FUNCTIONS.get(
524
+ kwargs.get("attn_implementation", "sdpa"), F.scaled_dot_product_attention
525
+ )
526
+
527
+ # Determining causality and windowing:
528
+ # 1. Training (T > 1): Use sliding window causal mask.
529
+ # 2. Decoding (T = 1): Use sliding window and NO CAUSAL MASK
530
+ if T > 1:
531
+ # Training/Prefill: Attend to full k, v but apply band-limited causal mask
532
+ # Build sliding window causal mask (T, kv_seq_len)
533
+ kv_all_seq_len = k.shape[2]
534
+ past_seq_len = kv_all_seq_len - T
535
+
536
+ mask = torch.zeros((T, kv_all_seq_len), device=x.device, dtype=x.dtype)
537
+
538
+ row_idx = torch.arange(T, device=x.device).view(-1, 1)
539
+ col_idx = torch.arange(kv_all_seq_len, device=x.device).view(1, -1)
540
+ abs_pos = row_idx + past_seq_len
541
+
542
+ # Causal upper triangle = -inf
543
+ mask = torch.where(col_idx > abs_pos, float("-inf"), mask)
544
+
545
+ # Keep tokens in range [abs_pos - self.window_size, abs_pos]
546
+ if self.window_size is not None:
547
+ mask = torch.where((abs_pos - col_idx) >= self.window_size, float("-inf"), mask)
548
+
549
+ # Replace -inf with 0 for the permitted window (float mask expected by sdpa)
550
+ mask = torch.where(mask == float("-inf"), mask, torch.zeros_like(mask))
551
+
552
+ y = attn_fn(q, k, v, attn_mask=mask.unsqueeze(0).unsqueeze(0))
553
+ else:
554
+ # Decoding: Recurrent step, attend only to the last window_size tokens
555
+ y = attn_fn(q, k_attn, v_attn, is_causal=False)
556
+
557
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
558
+ return self.out_proj(y), current_key_value
559
+
560
+
561
+ class DSRNBlock(nn.Module):
562
+ def __init__(self, config: EchoConfig):
563
+ super().__init__()
564
+ self.config = config
565
+ self.hidden_size = config.hidden_size
566
+ self.state_size = config.hidden_size * config.num_heads
567
+ self.use_triton = getattr(config, "use_triton", True)
568
+ self.use_hybrid_attention = getattr(config, "use_hybrid_attention", True)
569
+ self.use_rmsnorm = getattr(config, "use_rmsnorm", True)
570
+
571
+ # Fast State (GRU)
572
+ if self.use_rmsnorm:
573
+ self.norm_fast = HymbaRMSNorm(config.hidden_size)
574
+ else:
575
+ self.norm_fast = nn.LayerNorm(config.hidden_size)
576
+
577
+ self.gru_cell = nn.GRUCell(config.hidden_size, config.hidden_size)
578
+
579
+ # Hybrid Attention
580
+ if self.use_hybrid_attention:
581
+ self.attn = SlidingWindowAttention(config)
582
+
583
+ # Slow State (DSRN)
584
+ self.linear_read = nn.Linear(self.state_size, config.hidden_size, bias=False)
585
+ self.linear_gate = nn.Linear(config.hidden_size, self.state_size)
586
+ self.linear_memory = nn.Linear(config.hidden_size, self.state_size)
587
+
588
+ # -- Surprise Mechanism --
589
+ self.linear_pred = nn.Linear(config.hidden_size, config.hidden_size, bias=False)
590
+ self.surprise_lambda = nn.Parameter(torch.zeros(self.state_size))
591
+
592
+ # Feed-Forward
593
+ if self.use_rmsnorm:
594
+ self.norm_ff = HymbaRMSNorm(config.hidden_size)
595
+ else:
596
+ self.norm_ff = nn.LayerNorm(config.hidden_size)
597
+
598
+ # Simple MLP: Linear -> GELU -> Linear
599
+ # mlp_up / mlp_act / mlp_down are the ONLY registered submodules.
600
+ # No self.mlp alias β€” that caused double-registration and spurious "missing keys".
601
+ intermediate_size = getattr(
602
+ config, "intermediate_size", int(config.hidden_size * getattr(config, "mlp_ratio", 4.0))
603
+ )
604
+ # Use getattr guard so configs loaded from old JSON (pre-mlp_bias field) default safely.
605
+ _mlp_bias = getattr(config, "mlp_bias", False)
606
+ self.mlp_up = nn.Linear(config.hidden_size, intermediate_size, bias=_mlp_bias)
607
+ self.mlp_act = nn.GELU()
608
+ self.mlp_down = nn.Linear(intermediate_size, config.hidden_size, bias=_mlp_bias)
609
+
610
+ def forward(
611
+ self, x: torch.Tensor, state_prev: Tuple[torch.Tensor, ...], **kwargs
612
+ ) -> Tuple[torch.Tensor, Tuple[torch.Tensor, ...]]:
613
+
614
+ # Unpack state
615
+ # Supports (h, c) or (h, c, k_attn, v_attn)
616
+ h_prev = state_prev[0]
617
+ c_prev = state_prev[1]
618
+
619
+ if self.use_triton and x.is_cuda:
620
+ # Placeholder for Triton
621
+ pass
622
+
623
+ # Use Parallel Kernel
624
+ x_out, h_new, c_new, gate_stats = dsrn_parallel_kernel(self, x, h_prev, c_prev)
625
+
626
+ if self.use_hybrid_attention:
627
+ # Re-apply norm for attention branch (cleanest for surgical transplant)
628
+ x_norm = self.norm_fast(x)
629
+
630
+ # Extract attention state from tuple if present (h, c, k_attn, v_attn)
631
+ # HF state structure is now: (h, c, k_attn, v_attn)
632
+ # But wait, past_key_values in forward loop is just (h,c) from legacy code.
633
+ # We need to expand the state tuple to include attention KV.
634
+
635
+ attn_kv = None
636
+ if len(state_prev) == 4:
637
+ attn_kv = (state_prev[2], state_prev[3])
638
+
639
+ attn_out, new_attn_kv = self.attn(x_norm, past_key_values=attn_kv, **kwargs)
640
+ x_out = x_out + attn_out
641
+
642
+ # Update state with new KV
643
+ if new_attn_kv is not None:
644
+ h_new_full = (h_new, c_new, new_attn_kv[0], new_attn_kv[1])
645
+ else:
646
+ h_new_full = (h_new, c_new)
647
+ else:
648
+ h_new_full = (h_new, c_new)
649
+
650
+ return x_out, h_new_full, gate_stats
651
+
652
+
653
+ class EchoPreTrainedModel(PreTrainedModel):
654
+ config_class = EchoConfig
655
+ base_model_prefix = "model"
656
+ _no_split_modules = ["DSRNBlock"]
657
+
658
+ # Silently drop legacy mlp.0.*/mlp.1.*/mlp.2.* alias keys if they exist in old
659
+ # local training checkpoints from before the self.mlp aliasing was removed.
660
+ # The canonical names are mlp_up.* / mlp_act.* / mlp_down.* which load fine.
661
+ _keys_to_ignore_on_load_unexpected = [
662
+ r".*\.mlp\.0\..*",
663
+ r".*\.mlp\.1\..*",
664
+ r".*\.mlp\.2\..*",
665
+ ]
666
+
667
+ def _init_weights(self, module):
668
+ if isinstance(module, nn.Linear):
669
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
670
+ if module.bias is not None:
671
+ torch.nn.init.zeros_(module.bias)
672
+ elif isinstance(module, nn.Embedding):
673
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
674
+ elif isinstance(module, nn.LayerNorm):
675
+ torch.nn.init.zeros_(module.bias)
676
+ torch.nn.init.ones_(module.weight)
677
+
678
+
679
+ class EchoModel(EchoPreTrainedModel):
680
+ supports_gradient_checkpointing = True
681
+ _supports_attention_backend = True
682
+
683
+ def __init__(self, config: EchoConfig):
684
+ super().__init__(config)
685
+ self.embed_dim = config.embed_dim
686
+ self.num_layers = config.num_layers
687
+ self.num_heads = config.num_heads
688
+ self.state_dim = config.embed_dim * config.num_heads
689
+
690
+ self.embedding = nn.Embedding(config.vocab_size, config.embed_dim)
691
+ self.blocks = nn.ModuleList([DSRNBlock(config) for _ in range(config.num_layers)])
692
+
693
+ if getattr(config, "use_rmsnorm", False):
694
+ self.final_norm = HymbaRMSNorm(config.hidden_size)
695
+ else:
696
+ self.final_norm = nn.LayerNorm(config.hidden_size)
697
+
698
+ self.gradient_checkpointing = False
699
+
700
+ self.post_init()
701
+
702
+ # --- ZOMBIE GRADIENT PATCH (FIXED) ---
703
+ # Fixed: Now using controlled bias defaults to 1.0 to encourage open gates initially
704
+ bias_val = getattr(config, "gate_bias_init", 1.0)
705
+ for block in self.blocks:
706
+ nn.init.constant_(block.linear_gate.bias, bias_val)
707
+ # Init Surprise
708
+ if (
709
+ block.linear_pred.weight.dtype in (torch.bfloat16, torch.float16)
710
+ and block.linear_pred.weight.is_cuda
711
+ ):
712
+ _device = block.linear_pred.weight.device
713
+ _dtype = block.linear_pred.weight.dtype
714
+ temp_w = torch.empty_like(
715
+ block.linear_pred.weight, dtype=torch.float32, device="cpu"
716
+ )
717
+ nn.init.orthogonal_(temp_w, gain=0.1)
718
+ with torch.no_grad():
719
+ block.linear_pred.weight.copy_(temp_w.to(device=_device, dtype=_dtype))
720
+ else:
721
+ nn.init.orthogonal_(block.linear_pred.weight, gain=0.1)
722
+
723
+ nn.init.zeros_(block.surprise_lambda)
724
+ # CRITICAL: Zero-Init Residual Output (Identity Start)
725
+ nn.init.zeros_(block.mlp_down.weight)
726
+ if block.mlp_down.bias is not None:
727
+ nn.init.zeros_(block.mlp_down.bias)
728
+
729
+ def _set_gradient_checkpointing(self, enable=True, gradient_checkpointing_func=None):
730
+ """Enable/disable gradient checkpointing."""
731
+ self.gradient_checkpointing = enable
732
+
733
+ def get_input_embeddings(self):
734
+ return self.embedding
735
+
736
+ def set_input_embeddings(self, value):
737
+ self.embedding = value
738
+
739
+ def forward(
740
+ self,
741
+ input_ids: Optional[torch.LongTensor] = None,
742
+ past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
743
+ inputs_embeds: Optional[torch.FloatTensor] = None,
744
+ position_ids: Optional[torch.LongTensor] = None,
745
+ output_dsrn_telemetry: Optional[bool] = False,
746
+ output_attentions: Optional[bool] = None,
747
+ output_hidden_states: Optional[bool] = None,
748
+ return_dict: Optional[bool] = None,
749
+ **kwargs,
750
+ ) -> Union[Tuple, BaseModelOutputWithPast]:
751
+
752
+ return_dict = (
753
+ return_dict
754
+ if return_dict is not None
755
+ else getattr(self.config, "use_return_dict", True)
756
+ )
757
+
758
+ if input_ids is not None and inputs_embeds is not None:
759
+ raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time")
760
+ elif input_ids is not None:
761
+ batch_size, seq_len = input_ids.shape
762
+ x = self.embedding(input_ids)
763
+ elif inputs_embeds is not None:
764
+ batch_size, seq_len, _ = inputs_embeds.shape
765
+ x = inputs_embeds
766
+ else:
767
+ raise ValueError("You have to specify either input_ids or inputs_embeds")
768
+
769
+ device = x.device
770
+
771
+ # Initialize states if not provided or if it's an empty Cache object
772
+ is_empty_cache = (
773
+ hasattr(past_key_values, "get_seq_length") and past_key_values.get_seq_length() == 0
774
+ )
775
+ if past_key_values is None or is_empty_cache:
776
+ past_key_values = []
777
+ for _ in range(self.num_layers):
778
+ h = torch.zeros(batch_size, self.embed_dim, device=device, dtype=x.dtype)
779
+ c = torch.zeros(batch_size, self.state_dim, device=device, dtype=x.dtype)
780
+ past_key_values.append((h, c))
781
+
782
+ current_states = past_key_values
783
+ next_states = []
784
+
785
+ all_gate_stats = [] if output_dsrn_telemetry else None
786
+ all_c_states = [] if output_dsrn_telemetry else None
787
+
788
+ # Layer-Major Execution
789
+ for i, block in enumerate(self.blocks):
790
+
791
+ # Handle potential DynamicCache structure or list of tuples
792
+ if hasattr(current_states, "__getitem__"):
793
+ state_i = current_states[i]
794
+ else:
795
+ state_i = current_states[i]
796
+
797
+ if len(state_i) == 2:
798
+ # DSRN Only
799
+ pass
800
+ elif len(state_i) == 4:
801
+ # DSRN + Attention State
802
+ pass
803
+ else:
804
+ # Fallback for empty/malformed states
805
+ h_prev = torch.zeros(batch_size, self.embed_dim, device=device)
806
+ c_prev = torch.zeros(batch_size, self.state_dim, device=device)
807
+ state_i = (h_prev, c_prev)
808
+
809
+ # Use gradient checkpointing if enabled
810
+ if self.gradient_checkpointing and self.training:
811
+ # Checkpointing complex states is tricky, usually just pass h/c
812
+ out = torch.utils.checkpoint.checkpoint(block, x, state_i, use_reentrant=False)
813
+ else:
814
+ out = block(x, state_i, **kwargs)
815
+
816
+ x = out[0]
817
+ next_states.append(out[1])
818
+
819
+ if output_dsrn_telemetry:
820
+ all_gate_stats.append(out[2])
821
+ all_c_states.append(out[1][1])
822
+
823
+ x = self.final_norm(x)
824
+
825
+ if isinstance(current_states, EchoCache):
826
+ current_states.states = next_states
827
+ next_states = current_states
828
+ elif EchoCache is not None:
829
+ next_states = EchoCache(next_states)
830
+
831
+ # Revert to raw tuple outputs if return_dict=False is requested
832
+ if not return_dict:
833
+ if output_dsrn_telemetry:
834
+ return x, next_states, all_c_states, all_gate_stats
835
+ return x, next_states
836
+
837
+ # Standard HF Object wrapper containing last_hidden_state
838
+ output_obj = BaseModelOutputWithPast(
839
+ last_hidden_state=x,
840
+ past_key_values=next_states,
841
+ hidden_states=(x,) if output_hidden_states else None,
842
+ attentions=None,
843
+ )
844
+ if output_dsrn_telemetry:
845
+ output_obj.all_c_states = all_c_states
846
+ output_obj.all_gate_stats = all_gate_stats
847
+ return output_obj
848
+
849
+
850
+ class EchoForCausalLM(EchoPreTrainedModel, GenerationMixin):
851
+ _is_causal = True
852
+ supports_gradient_checkpointing = True
853
+ _supports_cache_class = False
854
+ _supports_static_cache = False
855
+ main_input_name = "input_ids"
856
+ # Required by the modern HF tie_weights() mechanism (transformers β‰₯ 4.47).
857
+ # Without this dict being non-None, tie_weights() returns early even when
858
+ # tie_word_embeddings=True and get_input/output_embeddings() are both defined.
859
+ _tied_weights_keys = {"lm_head.weight": "model.embedding.weight"}
860
+
861
+ @property
862
+ def _keys_to_ignore_on_load_missing(self):
863
+ # When mlp_bias=False (the default, and the setting for all v0.1.2 checkpoints),
864
+ # bias tensors are not present in the checkpoint and should not trigger warnings.
865
+ # When mlp_bias=True, these keys WILL exist in the checkpoint β€” do not silence them.
866
+ if not getattr(self.config, "mlp_bias", False):
867
+ return [r"model\.blocks\.\d+\.mlp_(up|down)\.bias"]
868
+ return []
869
+
870
+ @classmethod
871
+ def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
872
+ model = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
873
+
874
+ # Defense-in-depth: if mlp_bias=False but bias tensors were somehow initialized
875
+ # (e.g. an old code path created them), zero them out to prevent NaN/Inf
876
+ # corruption when running in bfloat16.
877
+ if not getattr(model.config, "mlp_bias", False):
878
+ zeroed = 0
879
+ with torch.no_grad():
880
+ for name, param in model.named_parameters():
881
+ if "mlp_up.bias" in name or "mlp_down.bias" in name:
882
+ param.zero_()
883
+ zeroed += 1
884
+ if zeroed:
885
+ import warnings
886
+
887
+ warnings.warn(
888
+ f"Zeroed {zeroed} MLP bias tensor(s) that were missing from the "
889
+ f"checkpoint. This indicates a config/checkpoint mismatch. "
890
+ f"Ensure mlp_bias=False in EchoConfig for v0.1.2 checkpoints.",
891
+ UserWarning,
892
+ )
893
+
894
+ return model
895
+
896
+ def __init__(self, config: EchoConfig):
897
+ super().__init__(config)
898
+ self.model = EchoModel(config)
899
+ self.lm_head = nn.Linear(config.embed_dim, config.vocab_size, bias=False)
900
+ self._latest_c_states = None
901
+ self._latest_gate_stats = None
902
+
903
+ # Initialize weights and apply final processing
904
+ self.post_init()
905
+
906
+ def get_input_embeddings(self):
907
+ return self.model.embedding
908
+
909
+ def set_input_embeddings(self, value):
910
+ self.model.embedding = value
911
+
912
+ def _set_gradient_checkpointing(self, enable=True, gradient_checkpointing_func=None):
913
+ """Enable/disable gradient checkpointing."""
914
+ self.model._set_gradient_checkpointing(enable, gradient_checkpointing_func)
915
+
916
+ def get_output_embeddings(self):
917
+ return self.lm_head
918
+
919
+ def set_output_embeddings(self, new_embeddings):
920
+ self.lm_head = new_embeddings
921
+
922
+ def forward(
923
+ self,
924
+ input_ids: torch.LongTensor,
925
+ attention_mask: Optional[torch.LongTensor] = None,
926
+ position_ids: Optional[torch.LongTensor] = None,
927
+ past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
928
+ inputs_embeds: Optional[torch.FloatTensor] = None,
929
+ labels: Optional[torch.LongTensor] = None,
930
+ use_cache: Optional[bool] = None,
931
+ output_attentions: Optional[bool] = None,
932
+ output_hidden_states: Optional[bool] = None,
933
+ return_dict: Optional[bool] = None,
934
+ output_dsrn_telemetry: Optional[bool] = False,
935
+ **kwargs,
936
+ ) -> Union[Tuple, CausalLMOutputWithPast]:
937
+
938
+ output_attentions = (
939
+ output_attentions
940
+ if output_attentions is not None
941
+ else getattr(self.config, "output_attentions", False)
942
+ )
943
+ output_hidden_states = (
944
+ output_hidden_states
945
+ if output_hidden_states is not None
946
+ else getattr(self.config, "output_hidden_states", False)
947
+ )
948
+ use_cache = use_cache if use_cache is not None else getattr(self.config, "use_cache", True)
949
+
950
+ return_dict = (
951
+ return_dict
952
+ if return_dict is not None
953
+ else getattr(self.config, "use_return_dict", True)
954
+ )
955
+
956
+ '''
957
+ If kwargs is getting overloaded with extra args HF generate passes,
958
+ we safely extract kwargs here.
959
+ '''
960
+ # Pass position_ids explicitly alongside **kwargs
961
+ kwargs["position_ids"] = position_ids
962
+
963
+ # Call the base EchoModel
964
+ model_out = self.model(
965
+ input_ids=input_ids,
966
+ past_key_values=past_key_values,
967
+ inputs_embeds=inputs_embeds,
968
+ output_dsrn_telemetry=output_dsrn_telemetry,
969
+ output_attentions=output_attentions,
970
+ output_hidden_states=output_hidden_states,
971
+ return_dict=return_dict, # Pass return_dict explicitly
972
+ **kwargs,
973
+ )
974
+
975
+ # Handle BaseModelOutputWithPast or raw tuple output gracefully
976
+ if hasattr(model_out, "last_hidden_state"):
977
+ hidden_states = model_out.last_hidden_state
978
+ new_states = model_out.past_key_values
979
+ else:
980
+ hidden_states = model_out[0]
981
+ new_states = model_out[1]
982
+
983
+ # Extract telemetry if model returned raw tuple (or via custom properties)
984
+ if hasattr(model_out, "all_c_states"):
985
+ self._latest_c_states = model_out.all_c_states
986
+ self._latest_gate_stats = model_out.all_gate_stats
987
+ elif isinstance(model_out, tuple) and len(model_out) > 2:
988
+ self._latest_c_states = model_out[2]
989
+ self._latest_gate_stats = model_out[3]
990
+
991
+ # Project using Causal LM head
992
+ logits = self.lm_head(hidden_states)
993
+
994
+ loss = None
995
+ if labels is not None:
996
+ # Shift so that tokens < n predict n
997
+ shift_logits = logits[..., :-1, :].contiguous()
998
+ shift_labels = labels[..., 1:].contiguous()
999
+ loss_fct = nn.CrossEntropyLoss()
1000
+ loss = loss_fct(shift_logits.view(-1, self.config.vocab_size), shift_labels.view(-1))
1001
+
1002
+ if not return_dict:
1003
+ output = (logits, new_states)
1004
+ return ((loss,) + output) if loss is not None else output
1005
+
1006
+ return CausalLMOutputWithPast(
1007
+ loss=loss,
1008
+ logits=logits,
1009
+ past_key_values=new_states if use_cache else None,
1010
+ hidden_states=(hidden_states,) if output_hidden_states else None,
1011
+ attentions=None,
1012
+ )
1013
+
1014
+ def prepare_inputs_for_generation(
1015
+ self, input_ids, past_key_values=None, attention_mask=None, **kwargs
1016
+ ):
1017
+ # If past_key_values is a DynamicCache, we need to extract the underlying list of tuples
1018
+ # if the custom cache hasn't taken over yet. But actually, HF doesn't know about our 4-tuples.
1019
+ # So we should just let EchoModel handle it. If HF gave us a DynamicCache, it might be empty
1020
+ # or mangled.
1021
+ if (
1022
+ past_key_values is not None
1023
+ and not isinstance(past_key_values, (list, tuple))
1024
+ and not isinstance(past_key_values, EchoCache)
1025
+ ):
1026
+ # It's a DynamicCache. It's likely from the first generation step.
1027
+ # We can't use it directly because it stripped our (h,c).
1028
+ # But wait, on the VERY first generation step, past_key_values is None, then EchoModel returns EchoCache.
1029
+ # On subsequent steps we get EchoCache.
1030
+ # So if we get a DynamicCache, it means someone passed past_key_values explicitly to generate(),
1031
+ # or HF auto-created it on step 0 and passed it to step 1 incorrectly.
1032
+ pass
1033
+
1034
+ # In newer transformers, past_key_values could be a DynamicCache.
1035
+ # Check if it's effectively empty.
1036
+ is_empty = False
1037
+ if past_key_values is None:
1038
+ is_empty = True
1039
+ elif hasattr(past_key_values, "get_seq_length") and past_key_values.get_seq_length() == 0:
1040
+ is_empty = True
1041
+ elif isinstance(past_key_values, list) and len(past_key_values) == 0:
1042
+ is_empty = True
1043
+
1044
+ # If past_key_values is used, we only need the last token
1045
+ if not is_empty:
1046
+ input_ids = input_ids[:, -1:]
1047
+
1048
+ model_inputs = {
1049
+ "input_ids": input_ids,
1050
+ "past_key_values": past_key_values,
1051
+ "attention_mask": attention_mask,
1052
+ "use_cache": kwargs.get("use_cache"),
1053
+ }
1054
+
1055
+ # Pass through extra kwargs like output_dsrn_telemetry
1056
+ model_inputs.update({k: v for k, v in kwargs.items() if k not in model_inputs})
1057
+
1058
+ return model_inputs
1059
+
1060
+ def _reorder_cache(self, past_key_values, beam_idx):
1061
+ """
1062
+ Reorders cache for beam search or contrastive search.
1063
+ past_key_values: List[Tuple(h, c, ...)]
1064
+ """
1065
+ if past_key_values is None:
1066
+ return None
1067
+
1068
+ reordered_past = []
1069
+ for layer_past in past_key_values:
1070
+ # Each layer_past is a tuple of tensors (h, c) or (h, c, k, v)
1071
+ reordered_layer_past = tuple(
1072
+ p.index_select(0, beam_idx.to(p.device)) for p in layer_past
1073
+ )
1074
+ reordered_past.append(reordered_layer_past)
1075
+ return reordered_past
1076
+
1077
+
1078
+ class EchoClassifier(nn.Linear):
1079
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
1080
+ res = super().forward(input)
1081
+ if res.ndim == 3 and res.size(1) == 1:
1082
+ res = res.squeeze(1)
1083
+ return res
1084
+
1085
+
1086
+ class EchoForSequenceClassification(EchoPreTrainedModel):
1087
+ """
1088
+ Echo-DSRN with a sequence-level classification head.
1089
+
1090
+ This model is the *terminal* form of a fine-tuned classifier: it exposes
1091
+ only a ``classify()`` convenience method and a standard HF ``forward()``
1092
+ that returns :class:`~transformers.modeling_outputs.SequenceClassifierOutputWithPast`.
1093
+ It intentionally does **not** inherit :class:`~transformers.GenerationMixin` so
1094
+ chat-completion endpoints cannot be used accidentally.
1095
+
1096
+ Typical construction path
1097
+ -------------------------
1098
+ 1. Load ``EchoForCausalLM`` + LoRA adapter via :func:`merge_and_export`
1099
+ (see ``scripts/merge_clf_adapter.py``).
1100
+ 2. The resulting merged weights are saved as ``EchoForSequenceClassification``
1101
+ alongside a ``config.json`` that carries ``num_labels``, ``id2label``, and
1102
+ ``label2id``.
1103
+ 3. End-users load with::
1104
+
1105
+ from echo_dsrn import EchoForSequenceClassification
1106
+ model = EchoForSequenceClassification.from_pretrained("your/hub-id")
1107
+ label, probs = model.classify("some text")
1108
+ """
1109
+
1110
+ # Do NOT add GenerationMixin β€” this model must not generate text.
1111
+ main_input_name = "input_ids"
1112
+
1113
+ def __init__(self, config: EchoConfig):
1114
+ super().__init__(config)
1115
+ self.num_labels = getattr(config, "num_labels", 2)
1116
+ self.model = EchoModel(config)
1117
+
1118
+ classifier_dropout = getattr(config, "classifier_dropout", 0.0)
1119
+ self.dropout = nn.Dropout(classifier_dropout) if classifier_dropout > 0.0 else nn.Identity()
1120
+ self.classifier = EchoClassifier(config.embed_dim, self.num_labels, bias=True)
1121
+
1122
+ self.post_init()
1123
+
1124
+ @property
1125
+ def score(self) -> EchoClassifier:
1126
+ return self.classifier
1127
+
1128
+ @score.setter
1129
+ def score(self, value: EchoClassifier):
1130
+ self.classifier = value
1131
+
1132
+ # ------------------------------------------------------------------
1133
+ # HF embedding hooks (required by PreTrainedModel)
1134
+ # ------------------------------------------------------------------
1135
+ def get_input_embeddings(self):
1136
+ return self.model.embedding
1137
+
1138
+ def set_input_embeddings(self, value):
1139
+ self.model.embedding = value
1140
+
1141
+ def _set_gradient_checkpointing(self, enable=True, gradient_checkpointing_func=None):
1142
+ self.model._set_gradient_checkpointing(enable, gradient_checkpointing_func)
1143
+
1144
+ # ------------------------------------------------------------------
1145
+ # Forward
1146
+ # ------------------------------------------------------------------
1147
+ def forward(
1148
+ self,
1149
+ input_ids: Optional[torch.LongTensor] = None,
1150
+ attention_mask: Optional[torch.LongTensor] = None,
1151
+ position_ids: Optional[torch.LongTensor] = None,
1152
+ past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
1153
+ inputs_embeds: Optional[torch.FloatTensor] = None,
1154
+ labels: Optional[torch.LongTensor] = None,
1155
+ use_cache: Optional[bool] = None,
1156
+ output_hidden_states: Optional[bool] = None,
1157
+ return_dict: Optional[bool] = None,
1158
+ **kwargs,
1159
+ ) -> Union[Tuple, SequenceClassifierOutputWithPast]:
1160
+ """
1161
+ Parameters
1162
+ ----------
1163
+ labels:
1164
+ - ``num_labels == 1``: regression target (``torch.float``).
1165
+ - ``num_labels > 1``, single integer per sample: cross-entropy class index.
1166
+ - ``num_labels > 1``, float vector per sample: multi-label BCE.
1167
+ """
1168
+ return_dict = (
1169
+ return_dict
1170
+ if return_dict is not None
1171
+ else getattr(self.config, "use_return_dict", True)
1172
+ )
1173
+
1174
+ kwargs["position_ids"] = position_ids
1175
+
1176
+ model_out = self.model(
1177
+ input_ids=input_ids,
1178
+ past_key_values=past_key_values,
1179
+ inputs_embeds=inputs_embeds,
1180
+ **kwargs,
1181
+ )
1182
+
1183
+ hidden_states = model_out[0] # (B, T, D)
1184
+ new_states = model_out[1]
1185
+
1186
+ # --- Pooling: last non-padding token ---
1187
+ if attention_mask is not None:
1188
+ # Find the index of the last 1 in each row of attention_mask
1189
+ seq_lengths = attention_mask.sum(dim=1) - 1 # (B,)
1190
+ seq_lengths = seq_lengths.clamp(min=0)
1191
+ else:
1192
+ # No mask: use the true last token
1193
+ if input_ids is not None:
1194
+ seq_lengths = torch.full(
1195
+ (hidden_states.size(0),),
1196
+ hidden_states.size(1) - 1,
1197
+ dtype=torch.long,
1198
+ device=hidden_states.device,
1199
+ )
1200
+ else:
1201
+ seq_lengths = torch.full(
1202
+ (hidden_states.size(0),),
1203
+ hidden_states.size(1) - 1,
1204
+ dtype=torch.long,
1205
+ device=hidden_states.device,
1206
+ )
1207
+
1208
+ # Gather last-token hidden states: (B, D)
1209
+ pooled = hidden_states[
1210
+ torch.arange(hidden_states.size(0), device=hidden_states.device), seq_lengths
1211
+ ]
1212
+ pooled = self.dropout(pooled)
1213
+ logits = self.classifier(pooled) # (B, num_labels)
1214
+
1215
+ # --- Loss ---
1216
+ loss = None
1217
+ if labels is not None:
1218
+ if self.num_labels == 1:
1219
+ # Regression
1220
+ loss_fct = nn.MSELoss()
1221
+ loss = loss_fct(logits.squeeze(-1), labels.float())
1222
+ elif labels.dtype in (torch.float, torch.float16, torch.bfloat16):
1223
+ # Multi-label binary classification
1224
+ loss_fct = nn.BCEWithLogitsLoss()
1225
+ loss = loss_fct(logits, labels.float())
1226
+ else:
1227
+ # Standard multi-class
1228
+ loss_fct = nn.CrossEntropyLoss()
1229
+ loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
1230
+
1231
+ if not return_dict:
1232
+ output = (logits, new_states)
1233
+ return ((loss,) + output) if loss is not None else output
1234
+
1235
+ return SequenceClassifierOutputWithPast(
1236
+ loss=loss,
1237
+ logits=logits,
1238
+ past_key_values=new_states if use_cache else None,
1239
+ hidden_states=None,
1240
+ attentions=None,
1241
+ )
1242
+
1243
+ # ------------------------------------------------------------------
1244
+ # Convenience inference API
1245
+ # ------------------------------------------------------------------
1246
+ @torch.inference_mode()
1247
+ def classify(
1248
+ self,
1249
+ text: str,
1250
+ tokenizer,
1251
+ device: Optional[str] = None,
1252
+ return_probabilities: bool = True,
1253
+ ) -> Tuple[str, Optional[torch.Tensor]]:
1254
+ """
1255
+ High-level classification helper.
1256
+
1257
+ Parameters
1258
+ ----------
1259
+ text:
1260
+ Raw string to classify.
1261
+ tokenizer:
1262
+ A HuggingFace ``PreTrainedTokenizer`` compatible with the model.
1263
+ device:
1264
+ Optional device string (e.g. ``"cuda"``). Defaults to the device
1265
+ of the model's first parameter.
1266
+ return_probabilities:
1267
+ If ``True`` (default), also return a probability tensor (softmax
1268
+ for multi-class, sigmoid for binary/multi-label).
1269
+
1270
+ Returns
1271
+ -------
1272
+ label : str
1273
+ The predicted label string from ``config.id2label``.
1274
+ probabilities : Tensor or None
1275
+ Shape ``(num_labels,)`` probability vector, or ``None`` if
1276
+ ``return_probabilities=False``.
1277
+ """
1278
+ if device is None:
1279
+ try:
1280
+ device = str(next(self.parameters()).device)
1281
+ except StopIteration:
1282
+ device = "cpu"
1283
+
1284
+ self.eval()
1285
+
1286
+ # Format text if baked-in templates exist
1287
+ sys_prompt = getattr(self.config, "system_prompt", None)
1288
+ usr_template = getattr(self.config, "user_template", None)
1289
+
1290
+ if sys_prompt and usr_template:
1291
+ messages = [{"role": "system", "content": sys_prompt}]
1292
+ messages.append({"role": "user", "content": usr_template.format(text=text)})
1293
+ # Format using the tokenizer's chat template
1294
+ try:
1295
+ formatted_text = tokenizer.apply_chat_template(
1296
+ messages, add_generation_prompt=True, tokenize=False
1297
+ )
1298
+ except Exception:
1299
+ formatted_text = text
1300
+ else:
1301
+ formatted_text = text
1302
+
1303
+ enc = tokenizer(formatted_text, return_tensors="pt", truncation=True)
1304
+ enc = {k: v.to(device) for k, v in enc.items()}
1305
+
1306
+ output = self(**enc)
1307
+ logits = output.logits # (1, num_labels)
1308
+
1309
+ if self.num_labels == 1:
1310
+ # Regression: return raw value
1311
+ pred_label = str(logits.squeeze().item())
1312
+ probs = None
1313
+ elif self.num_labels == 2:
1314
+ probs_t = torch.softmax(logits, dim=-1).squeeze(0) if return_probabilities else None
1315
+ pred_id = int(logits.argmax(dim=-1).item())
1316
+ pred_label = getattr(self.config, "id2label", {0: "0", 1: "1"}).get(
1317
+ pred_id, str(pred_id)
1318
+ )
1319
+ probs = probs_t
1320
+ else:
1321
+ probs_t = torch.softmax(logits, dim=-1).squeeze(0) if return_probabilities else None
1322
+ pred_id = int(logits.argmax(dim=-1).item())
1323
+ pred_label = getattr(self.config, "id2label", {}).get(pred_id, str(pred_id))
1324
+ probs = probs_t
1325
+
1326
+ return pred_label, probs
1327
+
1328
+ @classmethod
1329
+ def from_causal_lm(
1330
+ cls,
1331
+ causal_lm_model,
1332
+ num_labels: int = 2,
1333
+ id2label: Optional[dict] = None,
1334
+ label2id: Optional[dict] = None,
1335
+ classifier_dropout: float = 0.0,
1336
+ label_token_ids: Optional[List[int]] = None,
1337
+ system_prompt: Optional[str] = None,
1338
+ user_template: Optional[str] = None,
1339
+ ) -> "EchoForSequenceClassification":
1340
+ """
1341
+ Construct an :class:`EchoForSequenceClassification` from a fully
1342
+ merged :class:`EchoForCausalLM` instance (i.e. after LoRA weights
1343
+ have been merged via ``peft.merge_adapter``).
1344
+
1345
+ The backbone weights are copied; the ``lm_head`` is discarded.
1346
+
1347
+ Classifier head initialisation
1348
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1349
+ If ``label_token_ids`` is provided (one token ID per class), the
1350
+ classifier weight rows are seeded directly from the corresponding
1351
+ ``lm_head`` weight rows. This is the correct initialisation for
1352
+ **generative** adapters that were fine-tuned to emit a label token
1353
+ (e.g. ``"0"`` or ``"1"``): the backbone already knows how to push
1354
+ the last hidden state toward those tokens, so we preserve that signal
1355
+ instead of starting from random.
1356
+
1357
+ Parameters
1358
+ ----------
1359
+ causal_lm_model:
1360
+ A loaded (and optionally LoRA-merged) ``EchoForCausalLM`` instance.
1361
+ num_labels:
1362
+ Number of output classes.
1363
+ id2label:
1364
+ Optional mapping ``{int -> str}`` for label names.
1365
+ label2id:
1366
+ Optional reverse mapping ``{str -> int}``.
1367
+ classifier_dropout:
1368
+ Dropout probability before the classification head.
1369
+ label_token_ids:
1370
+ Optional list of ``num_labels`` token IDs. When supplied, row
1371
+ ``i`` of the ``lm_head`` weight matrix is copied into row ``i``
1372
+ of the classifier weight matrix, seeding the head from the
1373
+ causal model's learned token distributions.
1374
+ Example for Echo-DSRN NSFW adapter::
1375
+
1376
+ label_token_ids=[29900, 29896] # token IDs for "0" and "1"
1377
+
1378
+ Returns
1379
+ -------
1380
+ EchoForSequenceClassification
1381
+ """
1382
+ if id2label is None:
1383
+ id2label = {i: str(i) for i in range(num_labels)}
1384
+ if label2id is None:
1385
+ label2id = {v: k for k, v in id2label.items()}
1386
+
1387
+ # Validate label_token_ids length
1388
+ if label_token_ids is not None and len(label_token_ids) != num_labels:
1389
+ raise ValueError(
1390
+ f"label_token_ids has {len(label_token_ids)} entries but num_labels={num_labels}. "
1391
+ "Must provide exactly one token ID per class."
1392
+ )
1393
+
1394
+ # Clone config and inject classification fields
1395
+ config = causal_lm_model.config
1396
+ config.num_labels = num_labels
1397
+ config.id2label = {int(k): v for k, v in id2label.items()}
1398
+ config.label2id = label2id
1399
+ config.classifier_dropout = classifier_dropout
1400
+
1401
+ if system_prompt is not None:
1402
+ config.system_prompt = system_prompt
1403
+ if user_template is not None:
1404
+ config.user_template = user_template
1405
+
1406
+ # Carry dtype forward so save_pretrained serialises it correctly
1407
+ if hasattr(causal_lm_model, "dtype"):
1408
+ config.torch_dtype = str(causal_lm_model.dtype).replace("torch.", "")
1409
+ # Update auto_map so Hub users get the right class on from_pretrained
1410
+ config.auto_map = {
1411
+ "AutoConfig": "configuration_echo.EchoConfig",
1412
+ "AutoModel": "modeling_echo.EchoModel",
1413
+ "AutoModelForSequenceClassification": ("modeling_echo.EchoForSequenceClassification"),
1414
+ }
1415
+
1416
+ # Build the classifier wrapper
1417
+ clf_model = cls(config)
1418
+
1419
+ # Copy backbone weights
1420
+ backbone_sd = causal_lm_model.model.state_dict()
1421
+ missing, unexpected = clf_model.model.load_state_dict(backbone_sd, strict=True)
1422
+ if missing:
1423
+ import warnings
1424
+
1425
+ warnings.warn(
1426
+ f"EchoForSequenceClassification.from_causal_lm: "
1427
+ f"missing backbone keys: {missing}",
1428
+ UserWarning,
1429
+ )
1430
+ if unexpected:
1431
+ import warnings
1432
+
1433
+ warnings.warn(
1434
+ f"EchoForSequenceClassification.from_causal_lm: "
1435
+ f"unexpected backbone keys: {unexpected}",
1436
+ UserWarning,
1437
+ )
1438
+
1439
+ # --- Seed classifier head from lm_head rows (generative adapter path) ---
1440
+ if label_token_ids is not None:
1441
+ lm_head_weight = causal_lm_model.lm_head.weight # (vocab_size, embed_dim)
1442
+ with torch.no_grad():
1443
+ for label_idx, token_id in enumerate(label_token_ids):
1444
+ clf_model.classifier.weight[label_idx].copy_(lm_head_weight[token_id])
1445
+ # Zero-init bias so initial scores are purely from the weight rows
1446
+ torch.nn.init.zeros_(clf_model.classifier.bias)
1447
+
1448
+ # --- Cast entire model to the source dtype ---
1449
+ # cls(config) initialises weights in float32 by default.
1450
+ # We cast everything uniformly AFTER all weight copies so that both
1451
+ # the backbone and the seeded classifier head end up in the same precision.
1452
+ src_dtype = causal_lm_model.dtype # e.g. torch.bfloat16
1453
+ if src_dtype != torch.float32:
1454
+ clf_model = clf_model.to(src_dtype)
1455
+ # Persist in config using the current (non-deprecated) field name
1456
+ config.dtype = str(src_dtype).replace("torch.", "")
1457
+
1458
+ return clf_model
triton_scan.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import triton
3
+ import triton.language as tl
4
+
5
+ # ──────────────────────────────────────────────────────────────
6
+ # FORWARD PASS KERNELS
7
+ # ──────────────────────────────────────────────────────────────
8
+
9
+
10
+ @triton.jit
11
+ def fwd_accumulate_kernel(
12
+ a_ptr,
13
+ b_ptr,
14
+ chunk_a_ptr,
15
+ chunk_c_ptr,
16
+ T,
17
+ D,
18
+ stride_a_b,
19
+ stride_a_t,
20
+ stride_a_d,
21
+ stride_b_b,
22
+ stride_b_t,
23
+ stride_b_d,
24
+ BLOCK_SIZE_D: tl.constexpr,
25
+ BLOCK_SIZE_T: tl.constexpr,
26
+ ):
27
+ pid_b = tl.program_id(0)
28
+ pid_d = tl.program_id(1)
29
+ pid_t = tl.program_id(2)
30
+
31
+ d_offsets = pid_d * BLOCK_SIZE_D + tl.arange(0, BLOCK_SIZE_D)
32
+ d_mask = d_offsets < D
33
+
34
+ # Chunk boundaries
35
+ t_start = pid_t * BLOCK_SIZE_T
36
+
37
+ # Initialize local carries
38
+ a_acc = tl.full((BLOCK_SIZE_D,), 1.0, dtype=tl.float32)
39
+ c_acc = tl.zeros((BLOCK_SIZE_D,), dtype=tl.float32)
40
+
41
+ a_base = a_ptr + pid_b * stride_a_b + d_offsets * stride_a_d
42
+ b_base = b_ptr + pid_b * stride_b_b + d_offsets * stride_b_d
43
+
44
+ for i in range(BLOCK_SIZE_T):
45
+ t = t_start + i
46
+ if t < T:
47
+ a = tl.load(a_base + t * stride_a_t, mask=d_mask, other=1.0).to(tl.float32)
48
+ b = tl.load(b_base + t * stride_b_t, mask=d_mask, other=0.0).to(tl.float32)
49
+
50
+ # Combine: (a_acc, c_acc) o (a, b) = (a * a_acc, a * c_acc + b)
51
+ c_acc = a * c_acc + b
52
+ a_acc = a * a_acc
53
+
54
+ # Store chunk summaries
55
+ # chunk_ptr: [B, num_chunks, D]
56
+ num_chunks = (T + BLOCK_SIZE_T - 1) // BLOCK_SIZE_T
57
+ summary_idx = pid_b * (num_chunks * D) + pid_t * D + d_offsets
58
+ tl.store(chunk_a_ptr + summary_idx, a_acc, mask=d_mask)
59
+ tl.store(chunk_c_ptr + summary_idx, c_acc, mask=d_mask)
60
+
61
+
62
+ @triton.jit
63
+ def fwd_global_scan_kernel(
64
+ chunk_a_ptr,
65
+ chunk_c_ptr,
66
+ chunk_carries_ptr,
67
+ c_0_ptr,
68
+ num_chunks,
69
+ D,
70
+ stride_c0_b,
71
+ stride_c0_d,
72
+ HAS_C_0: tl.constexpr,
73
+ BLOCK_SIZE_D: tl.constexpr,
74
+ ):
75
+ pid_b = tl.program_id(0)
76
+ pid_d = tl.program_id(1)
77
+
78
+ d_offsets = pid_d * BLOCK_SIZE_D + tl.arange(0, BLOCK_SIZE_D)
79
+ d_mask = d_offsets < D
80
+
81
+ # Initial carry
82
+ carry = tl.zeros((BLOCK_SIZE_D,), dtype=tl.float32)
83
+ if HAS_C_0:
84
+ c0_ptrs = c_0_ptr + pid_b * stride_c0_b + d_offsets * stride_c0_d
85
+ carry = tl.load(c0_ptrs, mask=d_mask, other=0.0).to(tl.float32)
86
+
87
+ # Base pointers for chunk summaries
88
+ chunk_base = pid_b * (num_chunks * D) + d_offsets
89
+
90
+ for j in range(num_chunks):
91
+ # Store carry into chunk j (this is c_{j-1})
92
+ tl.store(chunk_carries_ptr + chunk_base + j * D, carry, mask=d_mask)
93
+
94
+ # Load chunk summary
95
+ a_sum = tl.load(chunk_a_ptr + chunk_base + j * D, mask=d_mask, other=1.0).to(tl.float32)
96
+ c_sum = tl.load(chunk_c_ptr + chunk_base + j * D, mask=d_mask, other=0.0).to(tl.float32)
97
+
98
+ # Update carry for chunk j+1
99
+ carry = a_sum * carry + c_sum
100
+
101
+
102
+ @triton.jit
103
+ def fwd_combine_kernel(
104
+ a_ptr,
105
+ b_ptr,
106
+ chunk_carries_ptr,
107
+ c_out_ptr,
108
+ T,
109
+ D,
110
+ stride_a_b,
111
+ stride_a_t,
112
+ stride_a_d,
113
+ stride_b_b,
114
+ stride_b_t,
115
+ stride_b_d,
116
+ stride_c_b,
117
+ stride_c_t,
118
+ stride_c_d,
119
+ BLOCK_SIZE_D: tl.constexpr,
120
+ BLOCK_SIZE_T: tl.constexpr,
121
+ ):
122
+ pid_b = tl.program_id(0)
123
+ pid_d = tl.program_id(1)
124
+ pid_t = tl.program_id(2)
125
+
126
+ d_offsets = pid_d * BLOCK_SIZE_D + tl.arange(0, BLOCK_SIZE_D)
127
+ d_mask = d_offsets < D
128
+
129
+ num_chunks = (T + BLOCK_SIZE_T - 1) // BLOCK_SIZE_T
130
+ t_start = pid_t * BLOCK_SIZE_T
131
+
132
+ # Load initial carry for this chunk
133
+ carry_idx = pid_b * (num_chunks * D) + pid_t * D + d_offsets
134
+ carry = tl.load(chunk_carries_ptr + carry_idx, mask=d_mask, other=0.0).to(tl.float32)
135
+
136
+ a_base = a_ptr + pid_b * stride_a_b + d_offsets * stride_a_d
137
+ b_base = b_ptr + pid_b * stride_b_b + d_offsets * stride_b_d
138
+ c_out_base = c_out_ptr + pid_b * stride_c_b + d_offsets * stride_c_d
139
+
140
+ for i in range(BLOCK_SIZE_T):
141
+ t = t_start + i
142
+ if t < T:
143
+ a = tl.load(a_base + t * stride_a_t, mask=d_mask, other=1.0).to(tl.float32)
144
+ b = tl.load(b_base + t * stride_b_t, mask=d_mask, other=0.0).to(tl.float32)
145
+
146
+ carry = a * carry + b
147
+ tl.store(c_out_base + t * stride_c_t, carry, mask=d_mask)
148
+
149
+
150
+ # ──────────────────────────────────────────────────────────────
151
+ # BACKWARD PASS KERNELS
152
+ # ──────────────────────────────────────────────────────────────
153
+
154
+
155
+ @triton.jit
156
+ def bwd_accumulate_kernel(
157
+ a_ptr,
158
+ grad_c_out_ptr,
159
+ chunk_a_prod_ptr,
160
+ chunk_g_sum_ptr,
161
+ T,
162
+ D,
163
+ stride_a_b,
164
+ stride_a_t,
165
+ stride_a_d,
166
+ stride_g_b,
167
+ stride_g_t,
168
+ stride_g_d,
169
+ BLOCK_SIZE_D: tl.constexpr,
170
+ BLOCK_SIZE_T: tl.constexpr,
171
+ ):
172
+ pid_b = tl.program_id(0)
173
+ pid_d = tl.program_id(1)
174
+ pid_t = tl.program_id(2)
175
+
176
+ d_offsets = pid_d * BLOCK_SIZE_D + tl.arange(0, BLOCK_SIZE_D)
177
+ d_mask = d_offsets < D
178
+
179
+ t_start = pid_t * BLOCK_SIZE_T
180
+ t_end = tl.minimum(t_start + BLOCK_SIZE_T, T)
181
+
182
+ a_prod = tl.full((BLOCK_SIZE_D,), 1.0, dtype=tl.float32)
183
+ g_sum = tl.zeros((BLOCK_SIZE_D,), dtype=tl.float32)
184
+
185
+ a_base = a_ptr + pid_b * stride_a_b + d_offsets * stride_a_d
186
+ g_base = grad_c_out_ptr + pid_b * stride_g_b + d_offsets * stride_g_d
187
+
188
+ # Reverse sequential accumulation for chunk summary
189
+ # grad_c_start = (g_start + a_start+1*g_start+1 + ...) + (a_start+1*...*a_end) * grad_c_end
190
+ # We iterate from t_end-1 down to t_start
191
+ for i in range(t_end - t_start - 1, -1, -1):
192
+ t = t_start + i
193
+ g = tl.load(g_base + t * stride_g_t, mask=d_mask, other=0.0).to(tl.float32)
194
+
195
+ # Multiplier is a_{t+1}. If t is T-1, multiplier is 1.0 (or 0 if we assume grad_c_T=0)
196
+ # Actually, for the very last token in sequence, grad_c_T is 0.
197
+ a_next = tl.full((BLOCK_SIZE_D,), 1.0, dtype=tl.float32)
198
+ if t + 1 < T:
199
+ a_next = tl.load(a_base + (t + 1) * stride_a_t, mask=d_mask, other=1.0).to(tl.float32)
200
+
201
+ # combine: g_sum = g + a_next * g_sum, a_prod = a_next * a_prod
202
+ g_sum = g + a_next * g_sum
203
+ a_prod = a_next * a_prod
204
+
205
+ num_chunks = (T + BLOCK_SIZE_T - 1) // BLOCK_SIZE_T
206
+ summary_idx = pid_b * (num_chunks * D) + pid_t * D + d_offsets
207
+ tl.store(chunk_a_prod_ptr + summary_idx, a_prod, mask=d_mask)
208
+ tl.store(chunk_g_sum_ptr + summary_idx, g_sum, mask=d_mask)
209
+
210
+
211
+ @triton.jit
212
+ def bwd_global_scan_kernel(
213
+ chunk_a_prod_ptr,
214
+ chunk_g_sum_ptr,
215
+ chunk_grad_carries_ptr,
216
+ num_chunks,
217
+ D,
218
+ BLOCK_SIZE_D: tl.constexpr,
219
+ ):
220
+ pid_b = tl.program_id(0)
221
+ pid_d = tl.program_id(1)
222
+
223
+ d_offsets = pid_d * BLOCK_SIZE_D + tl.arange(0, BLOCK_SIZE_D)
224
+ d_mask = d_offsets < D
225
+
226
+ grad_carry = tl.zeros((BLOCK_SIZE_D,), dtype=tl.float32)
227
+ chunk_base = pid_b * (num_chunks * D) + d_offsets
228
+
229
+ # Scan from last chunk to first
230
+ for j in range(num_chunks - 1, -1, -1):
231
+ # Store carry into chunk j (this is grad_c_{chunk_j_end})
232
+ tl.store(chunk_grad_carries_ptr + chunk_base + j * D, grad_carry, mask=d_mask)
233
+
234
+ a_prod = tl.load(chunk_a_prod_ptr + chunk_base + j * D, mask=d_mask, other=1.0).to(
235
+ tl.float32
236
+ )
237
+ g_sum = tl.load(chunk_g_sum_ptr + chunk_base + j * D, mask=d_mask, other=0.0).to(tl.float32)
238
+
239
+ # Update carry for chunk j-1
240
+ # grad_c_{t_start_of_chunk_j} = g_sum_chunk_j + a_prod_chunk_j * grad_c_{t_end_of_chunk_j}
241
+ grad_carry = g_sum + a_prod * grad_carry
242
+
243
+
244
+ @triton.jit
245
+ def bwd_combine_kernel(
246
+ a_ptr,
247
+ c_out_ptr,
248
+ c_0_ptr,
249
+ grad_c_out_ptr,
250
+ chunk_grad_carries_ptr,
251
+ grad_a_ptr,
252
+ grad_b_ptr,
253
+ grad_c_0_ptr,
254
+ T,
255
+ D,
256
+ stride_a_b,
257
+ stride_a_t,
258
+ stride_a_d,
259
+ stride_c_b,
260
+ stride_c_t,
261
+ stride_c_d,
262
+ stride_g_b,
263
+ stride_g_t,
264
+ stride_g_d,
265
+ stride_gb_b,
266
+ stride_gb_t,
267
+ stride_gb_d,
268
+ stride_c0_b,
269
+ stride_c0_d,
270
+ HAS_C_0: tl.constexpr,
271
+ BLOCK_SIZE_D: tl.constexpr,
272
+ BLOCK_SIZE_T: tl.constexpr,
273
+ ):
274
+ pid_b = tl.program_id(0)
275
+ pid_d = tl.program_id(1)
276
+ pid_t = tl.program_id(2)
277
+
278
+ d_offsets = pid_d * BLOCK_SIZE_D + tl.arange(0, BLOCK_SIZE_D)
279
+ d_mask = d_offsets < D
280
+
281
+ num_chunks = (T + BLOCK_SIZE_T - 1) // BLOCK_SIZE_T
282
+ t_start = pid_t * BLOCK_SIZE_T
283
+ t_end = tl.minimum(t_start + BLOCK_SIZE_T, T)
284
+
285
+ # Load initial gradient carry (this is grad_c_{t_end})
286
+ # This was computed as grad_c_end in Pass 2.
287
+ grad_at_tend = tl.load(
288
+ chunk_grad_carries_ptr + pid_b * (num_chunks * D) + pid_t * D + d_offsets,
289
+ mask=d_mask,
290
+ other=0.0,
291
+ ).to(tl.float32)
292
+
293
+ a_base = a_ptr + pid_b * stride_a_b + d_offsets * stride_a_d
294
+ c_out_base = c_out_ptr + pid_b * stride_c_b + d_offsets * stride_c_d
295
+ g_base = grad_c_out_ptr + pid_b * stride_g_b + d_offsets * stride_g_d
296
+ ga_base = grad_a_ptr + pid_b * stride_a_b + d_offsets * stride_a_d
297
+ gb_base = grad_b_ptr + pid_b * stride_gb_b + d_offsets * stride_gb_d
298
+
299
+ # running_grad enters index t as a_{t+1} * grad_c_{t+1}
300
+ # For the very last token in chunk t=t_end-1, we need a_{t_end} * grad_c_{t_end}
301
+ a_tend = tl.full((BLOCK_SIZE_D,), 1.0, dtype=tl.float32)
302
+ if t_end < T:
303
+ a_tend = tl.load(a_base + t_end * stride_a_t, mask=d_mask, other=1.0).to(tl.float32)
304
+
305
+ running_grad = a_tend * grad_at_tend
306
+
307
+ # Reverse scan within chunk
308
+ for i in range(t_end - t_start - 1, -1, -1):
309
+ t = t_start + i
310
+ g_out_t = tl.load(g_base + t * stride_g_t, mask=d_mask, other=0.0).to(tl.float32)
311
+
312
+ # grad_c_t = g_out_t + a_{t+1} * grad_c_{t+1}
313
+ # In our loop, running_grad is always (a_{t+1} * grad_c_{t+1})
314
+ grad_c_t = g_out_t + running_grad
315
+
316
+ # Store results
317
+ # grad_b_t = grad_c_t
318
+ tl.store(gb_base + t * stride_gb_t, grad_c_t, mask=d_mask)
319
+
320
+ # grad_a_t = c_{t-1} * grad_c_t
321
+ c_prev = tl.zeros((BLOCK_SIZE_D,), dtype=tl.float32)
322
+ if t > 0:
323
+ c_prev = tl.load(c_out_base + (t - 1) * stride_c_t, mask=d_mask, other=0.0).to(
324
+ tl.float32
325
+ )
326
+ elif HAS_C_0:
327
+ c_prev = tl.load(
328
+ c_0_ptr + pid_b * stride_c0_b + d_offsets * stride_c0_d, mask=d_mask, other=0.0
329
+ ).to(tl.float32)
330
+
331
+ tl.store(ga_base + t * stride_a_t, c_prev * grad_c_t, mask=d_mask)
332
+
333
+ # update running_grad for the next iteration (t-1)
334
+ # new running_grad = a_t * grad_c_t
335
+ a_t = tl.load(a_base + t * stride_a_t, mask=d_mask, other=1.0).to(tl.float32)
336
+ running_grad = a_t * grad_c_t
337
+
338
+ # Final carry for d_c0 if pid_t == 0
339
+ if pid_t == 0 and HAS_C_0:
340
+ # After loop for t=0, running_grad is a_0 * grad_c_0
341
+ tl.store(
342
+ grad_c_0_ptr + pid_b * stride_c0_b + d_offsets * stride_c0_d, running_grad, mask=d_mask
343
+ )
344
+
345
+
346
+ # ──────────────────────────────────────────────────────────────
347
+ # PYTORCH WRAPPER
348
+ # ──────────────────────────────────────────────────────────────
349
+
350
+
351
+ class DSRNScanTriton(torch.autograd.Function):
352
+ @staticmethod
353
+ def forward(ctx, a, b, c_0=None):
354
+ B, T, D = a.shape
355
+ device = a.device
356
+
357
+ a = a.contiguous()
358
+ b = b.contiguous()
359
+ if c_0 is not None:
360
+ c_0 = c_0.contiguous()
361
+
362
+ c_out = torch.empty_like(a)
363
+
364
+ BLOCK_SIZE_T = 64
365
+ BLOCK_SIZE_D = triton.next_power_of_2(min(128, D))
366
+ num_chunks = (T + BLOCK_SIZE_T - 1) // BLOCK_SIZE_T
367
+
368
+ # Temporary workspace
369
+ chunk_a = torch.empty((B, num_chunks, D), device=device, dtype=torch.float32)
370
+ chunk_c = torch.empty((B, num_chunks, D), device=device, dtype=torch.float32)
371
+ chunk_carries = torch.empty((B, num_chunks, D), device=device, dtype=torch.float32)
372
+
373
+ # Pass 1: Accumulate
374
+ grid1 = (B, triton.cdiv(D, BLOCK_SIZE_D), num_chunks)
375
+ fwd_accumulate_kernel[grid1](
376
+ a,
377
+ b,
378
+ chunk_a,
379
+ chunk_c,
380
+ T,
381
+ D,
382
+ a.stride(0),
383
+ a.stride(1),
384
+ a.stride(2),
385
+ b.stride(0),
386
+ b.stride(1),
387
+ b.stride(2),
388
+ BLOCK_SIZE_D,
389
+ BLOCK_SIZE_T,
390
+ )
391
+
392
+ # Pass 2: Global Scan
393
+ grid2 = (B, triton.cdiv(D, BLOCK_SIZE_D))
394
+ fwd_global_scan_kernel[grid2](
395
+ chunk_a,
396
+ chunk_c,
397
+ chunk_carries,
398
+ c_0,
399
+ num_chunks,
400
+ D,
401
+ c_0.stride(0) if c_0 is not None else 0,
402
+ c_0.stride(1) if c_0 is not None else 0,
403
+ HAS_C_0=(c_0 is not None),
404
+ BLOCK_SIZE_D=BLOCK_SIZE_D,
405
+ )
406
+
407
+ # Pass 3: Combine
408
+ fwd_combine_kernel[grid1](
409
+ a,
410
+ b,
411
+ chunk_carries,
412
+ c_out,
413
+ T,
414
+ D,
415
+ a.stride(0),
416
+ a.stride(1),
417
+ a.stride(2),
418
+ b.stride(0),
419
+ b.stride(1),
420
+ b.stride(2),
421
+ c_out.stride(0),
422
+ c_out.stride(1),
423
+ c_out.stride(2),
424
+ BLOCK_SIZE_D,
425
+ BLOCK_SIZE_T,
426
+ )
427
+
428
+ ctx.save_for_backward(a, c_out, c_0)
429
+ ctx.BLOCK_SIZE_T = BLOCK_SIZE_T
430
+ ctx.BLOCK_SIZE_D = BLOCK_SIZE_D
431
+
432
+ return c_out
433
+
434
+ @staticmethod
435
+ def backward(ctx, grad_c_out):
436
+ a, c_out, c_0 = ctx.saved_tensors
437
+ B, T, D = a.shape
438
+ device = a.device
439
+
440
+ grad_c_out = grad_c_out.contiguous()
441
+ grad_a = torch.empty_like(a)
442
+ grad_b = torch.empty_like(a)
443
+ grad_c_0 = torch.zeros_like(c_0) if c_0 is not None else None
444
+
445
+ BLOCK_SIZE_T = ctx.BLOCK_SIZE_T
446
+ BLOCK_SIZE_D = ctx.BLOCK_SIZE_D
447
+ num_chunks = (T + BLOCK_SIZE_T - 1) // BLOCK_SIZE_T
448
+
449
+ chunk_grad_a = torch.empty((B, num_chunks, D), device=device, dtype=torch.float32)
450
+ chunk_grad_x = torch.empty((B, num_chunks, D), device=device, dtype=torch.float32)
451
+ chunk_grad_carries = torch.empty((B, num_chunks, D), device=device, dtype=torch.float32)
452
+
453
+ grid1 = (B, triton.cdiv(D, BLOCK_SIZE_D), num_chunks)
454
+
455
+ # Pass 1: Accumulate
456
+ bwd_accumulate_kernel[grid1](
457
+ a,
458
+ grad_c_out,
459
+ chunk_grad_a,
460
+ chunk_grad_x,
461
+ T,
462
+ D,
463
+ a.stride(0),
464
+ a.stride(1),
465
+ a.stride(2),
466
+ grad_c_out.stride(0),
467
+ grad_c_out.stride(1),
468
+ grad_c_out.stride(2),
469
+ BLOCK_SIZE_D,
470
+ BLOCK_SIZE_T,
471
+ )
472
+
473
+ # Pass 2: Global Scan
474
+ grid2 = (B, triton.cdiv(D, BLOCK_SIZE_D))
475
+ bwd_global_scan_kernel[grid2](
476
+ chunk_grad_a, chunk_grad_x, chunk_grad_carries, num_chunks, D, BLOCK_SIZE_D
477
+ )
478
+
479
+ # Pass 3: Combine
480
+ bwd_combine_kernel[grid1](
481
+ a,
482
+ c_out,
483
+ c_0,
484
+ grad_c_out,
485
+ chunk_grad_carries,
486
+ grad_a,
487
+ grad_b,
488
+ grad_c_0,
489
+ T,
490
+ D,
491
+ a.stride(0),
492
+ a.stride(1),
493
+ a.stride(2),
494
+ c_out.stride(0),
495
+ c_out.stride(1),
496
+ c_out.stride(2),
497
+ grad_c_out.stride(0),
498
+ grad_c_out.stride(1),
499
+ grad_c_out.stride(2),
500
+ grad_b.stride(0),
501
+ grad_b.stride(1),
502
+ grad_b.stride(2),
503
+ c_0.stride(0) if c_0 is not None else 0,
504
+ c_0.stride(1) if c_0 is not None else 0,
505
+ HAS_C_0=(c_0 is not None),
506
+ BLOCK_SIZE_D=BLOCK_SIZE_D,
507
+ BLOCK_SIZE_T=BLOCK_SIZE_T,
508
+ )
509
+
510
+ return grad_a, grad_b, grad_c_0
511
+
512
+
513
+ def triton_dsrn_parallel_scan(g_t, m_t, c_0=None):
514
+ orig_dtype = g_t.dtype
515
+ a = (1.0 - g_t).float()
516
+ b = (g_t * m_t).float()
517
+ if c_0 is not None:
518
+ c_0 = c_0.float()
519
+
520
+ out = DSRNScanTriton.apply(a, b, c_0)
521
+ return out.to(orig_dtype)