SeaWolf-AI commited on
Commit
7504a0e
·
verified ·
1 Parent(s): 5a5d928

Upload aether_pkg/v2_attentions/nsa.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. aether_pkg/v2_attentions/nsa.py +254 -0
aether_pkg/v2_attentions/nsa.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NSA: Native Sparse Attention (PRODUCTION - causal-safe)
3
+ ========================================================
4
+
5
+ DeepSeek 2502.11089 (ACL 2025 Best Paper)
6
+ Hardware-Aligned and Natively Trainable Sparse Attention.
7
+
8
+ 3 branches with learnable gating:
9
+ 1. Compressed: block-mean K/V + causal block mask (FIX 5/7)
10
+ 2. Selected: top-k blocks (fallback to full causal)
11
+ 3. Sliding: local window with causal mask
12
+
13
+ 5/7 fixes applied to placeholder bugs:
14
+ - NSACompressBranch: added causal block mask (q at pos t can only see blocks fully <= t-1)
15
+ - Class renamed NSAttention -> NSAAttention (modeling compat)
16
+ - forward signature: layer_idx kwarg + (Tensor, past_kv) tuple return
17
+
18
+ NSAAttention is the public name imported by modeling_aether_v2_7way.py.
19
+ """
20
+ from __future__ import annotations
21
+ import math
22
+ from typing import Optional, Tuple
23
+
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+
28
+
29
+ class NSACompressBranch(nn.Module):
30
+ """Block-compressed long-range attention WITH causal block mask."""
31
+
32
+ def __init__(self, config):
33
+ super().__init__()
34
+ self.h = config.hidden_size
35
+ self.num_heads = config.num_attention_heads
36
+ self.num_kv_heads = config.num_key_value_heads
37
+ self.head_dim = config.head_dim
38
+ # accept either nsa_compressed_block_size or nsa_compress_block_size
39
+ self.block_size = getattr(config, "nsa_compressed_block_size",
40
+ getattr(config, "nsa_compress_block_size", 32))
41
+
42
+ self.q_proj = nn.Linear(self.h, self.num_heads * self.head_dim, bias=False)
43
+ self.k_proj = nn.Linear(self.h, self.num_kv_heads * self.head_dim, bias=False)
44
+ self.v_proj = nn.Linear(self.h, self.num_kv_heads * self.head_dim, bias=False)
45
+ self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.h, bias=False)
46
+
47
+ self.block_compress_k = nn.Linear(self.head_dim, self.head_dim, bias=False)
48
+ self.block_compress_v = nn.Linear(self.head_dim, self.head_dim, bias=False)
49
+
50
+ def compress_blocks(self, x: torch.Tensor) -> torch.Tensor:
51
+ """x: [B, S, H, D] -> [B, num_blocks, H, D] via block-mean (truncate non-aligned)."""
52
+ B, S, H, D = x.shape
53
+ num_blocks = S // self.block_size
54
+ if num_blocks == 0:
55
+ return x[:, :0] # empty
56
+ x = x[:, : num_blocks * self.block_size]
57
+ x = x.view(B, num_blocks, self.block_size, H, D)
58
+ return x.mean(dim=2)
59
+
60
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
61
+ B, S, _ = hidden_states.shape
62
+
63
+ q = self.q_proj(hidden_states).view(B, S, self.num_heads, self.head_dim)
64
+ k = self.k_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim)
65
+ v = self.v_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim)
66
+
67
+ # Compress K, V into blocks
68
+ k_compressed = self.compress_blocks(k) # [B, num_blocks, kv_h, D]
69
+ v_compressed = self.compress_blocks(v)
70
+ num_blocks = k_compressed.shape[1]
71
+
72
+ if num_blocks == 0:
73
+ # Sequence shorter than block_size — fallback to identity (no compress contribution)
74
+ out = torch.zeros(B, S, self.h, device=hidden_states.device, dtype=hidden_states.dtype)
75
+ return out
76
+
77
+ k_compressed = self.block_compress_k(k_compressed)
78
+ v_compressed = self.block_compress_v(v_compressed)
79
+
80
+ # GQA repeat
81
+ repeat = self.num_heads // self.num_kv_heads
82
+ k_compressed = k_compressed.repeat_interleave(repeat, dim=2)
83
+ v_compressed = v_compressed.repeat_interleave(repeat, dim=2)
84
+
85
+ # [B, H, S, D]
86
+ q = q.transpose(1, 2)
87
+ k_c = k_compressed.transpose(1, 2) # [B, H, num_blocks, D]
88
+ v_c = v_compressed.transpose(1, 2)
89
+
90
+ scores = torch.matmul(q, k_c.transpose(-2, -1)) / math.sqrt(self.head_dim)
91
+ # ★ FIX 5/7: causal block mask
92
+ # query at position t can attend to block i iff (i+1)*bs <= t (block fully completed by t-1)
93
+ # equivalently: block_end_inclusive = (i+1)*bs - 1 < t → i*bs + bs - 1 < t → i < (t - bs + 1) / bs
94
+ # simpler: mask True where block start > t (block has not started)... but we need stricter:
95
+ # block i is "in the past" iff its END (i+1)*bs - 1 < t, i.e. (i+1)*bs <= t.
96
+ q_pos = torch.arange(S, device=q.device).unsqueeze(1) # [S, 1]
97
+ block_end = (torch.arange(num_blocks, device=q.device) + 1) * self.block_size # [num_blocks]
98
+ block_end = block_end.unsqueeze(0) # [1, num_blocks]
99
+ # mask: True = blocked (future block)
100
+ mask = block_end > q_pos # [S, num_blocks], True if block_end > q_pos (block not fully past)
101
+ mask = mask.unsqueeze(0).unsqueeze(0) # [1, 1, S, num_blocks]
102
+ scores = scores.masked_fill(mask, float("-inf"))
103
+
104
+ # If a query has no past block, all -inf -> nan softmax. Set first block prob to 0 by giving it 0 logit.
105
+ # Cheap guard: rows that are all -inf get a 0-tensor output.
106
+ all_neg_inf = mask.all(dim=-1, keepdim=True) # [1,1,S,1]
107
+ # set those rows to a small finite value so softmax produces uniform; we'll zero output below
108
+ scores = torch.where(all_neg_inf.expand_as(scores), torch.zeros_like(scores), scores)
109
+
110
+ attn = F.softmax(scores, dim=-1)
111
+ out = torch.matmul(attn, v_c) # [B, H, S, D]
112
+ # Zero out positions that have no past block (would be invalid)
113
+ out = out.masked_fill(all_neg_inf, 0.0)
114
+ out = out.transpose(1, 2).reshape(B, S, -1)
115
+ return self.o_proj(out)
116
+
117
+
118
+ class NSASelectBranch(nn.Module):
119
+ """Top-k block selection. Currently falls back to full causal attention."""
120
+
121
+ def __init__(self, config):
122
+ super().__init__()
123
+ self.h = config.hidden_size
124
+ self.num_heads = config.num_attention_heads
125
+ self.num_kv_heads = config.num_key_value_heads
126
+ self.head_dim = config.head_dim
127
+ self.block_size = getattr(config, "nsa_compressed_block_size",
128
+ getattr(config, "nsa_compress_block_size", 32))
129
+ self.top_k = getattr(config, "nsa_selected_top_k",
130
+ getattr(config, "nsa_select_top_k", 16))
131
+
132
+ self.q_proj = nn.Linear(self.h, self.num_heads * self.head_dim, bias=False)
133
+ self.k_proj = nn.Linear(self.h, self.num_kv_heads * self.head_dim, bias=False)
134
+ self.v_proj = nn.Linear(self.h, self.num_kv_heads * self.head_dim, bias=False)
135
+ self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.h, bias=False)
136
+
137
+ self.block_score = nn.Linear(self.head_dim, 1, bias=False)
138
+
139
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
140
+ B, S, _ = hidden_states.shape
141
+
142
+ q = self.q_proj(hidden_states).view(B, S, self.num_heads, self.head_dim)
143
+ k = self.k_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim)
144
+ v = self.v_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim)
145
+
146
+ # GQA repeat
147
+ repeat = self.num_heads // self.num_kv_heads
148
+ k_full = k.repeat_interleave(repeat, dim=2)
149
+ v_full = v.repeat_interleave(repeat, dim=2)
150
+
151
+ # Use SDPA causal (FIX 5/7: strict causal)
152
+ q = q.transpose(1, 2)
153
+ k_full = k_full.transpose(1, 2)
154
+ v_full = v_full.transpose(1, 2)
155
+ out = F.scaled_dot_product_attention(q, k_full, v_full, dropout_p=0.0, is_causal=True)
156
+ out = out.transpose(1, 2).reshape(B, S, -1)
157
+ return self.o_proj(out)
158
+
159
+
160
+ class NSASlidingBranch(nn.Module):
161
+ """Local sliding window with causal mask."""
162
+
163
+ def __init__(self, config):
164
+ super().__init__()
165
+ self.h = config.hidden_size
166
+ self.num_heads = config.num_attention_heads
167
+ self.num_kv_heads = config.num_key_value_heads
168
+ self.head_dim = config.head_dim
169
+ self.window_size = getattr(config, "nsa_sliding_size",
170
+ getattr(config, "nsa_sliding_window", 512))
171
+
172
+ self.q_proj = nn.Linear(self.h, self.num_heads * self.head_dim, bias=False)
173
+ self.k_proj = nn.Linear(self.h, self.num_kv_heads * self.head_dim, bias=False)
174
+ self.v_proj = nn.Linear(self.h, self.num_kv_heads * self.head_dim, bias=False)
175
+ self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.h, bias=False)
176
+
177
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
178
+ B, S, _ = hidden_states.shape
179
+
180
+ q = self.q_proj(hidden_states).view(B, S, self.num_heads, self.head_dim)
181
+ k = self.k_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim)
182
+ v = self.v_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim)
183
+
184
+ repeat = self.num_heads // self.num_kv_heads
185
+ k = k.repeat_interleave(repeat, dim=2)
186
+ v = v.repeat_interleave(repeat, dim=2)
187
+
188
+ q = q.transpose(1, 2)
189
+ k = k.transpose(1, 2)
190
+ v = v.transpose(1, 2)
191
+
192
+ # Sliding window + causal
193
+ idx = torch.arange(S, device=q.device)
194
+ # mask: True = blocked
195
+ # idx[None,:] is k_pos (column), idx[:,None] is q_pos (row)
196
+ # block if k_pos > q_pos (future) or k_pos < q_pos - window + 1 (too far past)
197
+ mask = (idx[None, :] > idx[:, None]) | (idx[None, :] < idx[:, None] - self.window_size + 1)
198
+ scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
199
+ scores = scores.masked_fill(mask, float("-inf"))
200
+ attn = F.softmax(scores, dim=-1)
201
+ out = torch.matmul(attn, v)
202
+ out = out.transpose(1, 2).reshape(B, S, -1)
203
+ return self.o_proj(out)
204
+
205
+
206
+ class NSAAttention(nn.Module):
207
+ """
208
+ Native Sparse Attention (DeepSeek 2502.11089) — modeling-compat wrapper.
209
+
210
+ Public name: NSAAttention (matches modeling_aether_v2_7way.py import).
211
+ Accepts layer_idx kwarg (stored, currently unused).
212
+ Returns (Tensor, past_key_value=None) tuple to match other attention modules.
213
+ """
214
+
215
+ def __init__(self, config, layer_idx: int = 0):
216
+ super().__init__()
217
+ self.layer_idx = layer_idx
218
+ self.compressed = NSACompressBranch(config)
219
+ self.selected = NSASelectBranch(config)
220
+ self.sliding = NSASlidingBranch(config)
221
+
222
+ # Learnable gate (sigmoid normalized)
223
+ self.gate_c = nn.Parameter(torch.zeros(1))
224
+ self.gate_s = nn.Parameter(torch.zeros(1))
225
+ self.gate_w = nn.Parameter(torch.zeros(1))
226
+
227
+ self.norm = nn.LayerNorm(config.hidden_size, eps=config.rms_norm_eps)
228
+
229
+ def forward(
230
+ self,
231
+ hidden_states: torch.Tensor,
232
+ attention_mask: Optional[torch.Tensor] = None,
233
+ position_ids: Optional[torch.LongTensor] = None,
234
+ past_key_value=None,
235
+ use_cache: bool = False,
236
+ **kwargs,
237
+ ) -> Tuple[torch.Tensor, Optional[object]]:
238
+ out_c = self.compressed(hidden_states)
239
+ out_s = self.selected(hidden_states)
240
+ out_w = self.sliding(hidden_states)
241
+
242
+ g_c = torch.sigmoid(self.gate_c)
243
+ g_s = torch.sigmoid(self.gate_s)
244
+ g_w = torch.sigmoid(self.gate_w)
245
+
246
+ out = g_c * out_c + g_s * out_s + g_w * out_w
247
+ out = self.norm(out)
248
+ return out, past_key_value
249
+
250
+
251
+ # Backward compat alias (in case of NSAttention import)
252
+ NSAttention = NSAAttention
253
+
254
+ __all__ = ["NSAAttention", "NSAttention", "NSACompressBranch", "NSASelectBranch", "NSASlidingBranch"]