KitsuVp commited on
Commit
5d2ae2a
·
verified ·
1 Parent(s): 0c45277

Update modeling_neollm.py

Browse files
Files changed (1) hide show
  1. modeling_neollm.py +206 -69
modeling_neollm.py CHANGED
@@ -26,16 +26,18 @@ Attention stack (orthogonal, all active simultaneously when enabled):
26
  increases attention entropy, and is complementary to Gated Attention
27
  (Bae et al. 2026, Table 2: Affine-Scaled + Gated > either alone).
28
 
29
- Flash/SDPA path (exact, no approximation):
30
  Expanding [α·softmax(QKᵀ)+β]V distributively yields two terms:
31
- α · flash_attn(Q,K,V) — Flash computes this directly
32
- + β · Σ_{j≤i} V_j — causal prefix-sum of V (V.cumsum)
33
- The V_cumsum tensor [B,H,S,d_head] is the only extra memory vs a
34
- standard flash call. Semantically identical to the eager path;
35
- per-weight softmax tensors (attn_weights_pre/post_affine) are
36
- unavailable in flash mode and remain None in AnalysisState.
 
37
 
38
- Eager path: exact with full weight access, used for interpretability.
 
39
 
40
  References:
41
  FANformer: "FANformer: Improving Large Language Models Through Effective
@@ -86,7 +88,7 @@ from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_u
86
  from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
87
  from transformers.processing_utils import Unpack
88
  from transformers.utils import TransformersKwargs, logging
89
- from .configuration_neollm import NeoLLMConfig
90
 
91
  from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
92
  torch._dynamo.config.capture_scalar_outputs = True
@@ -351,7 +353,7 @@ class AttentionAnalysis:
351
  beta_per_head: Optional[torch.Tensor] = None # β [B,H,S,1] moving-avg bias
352
  alpha_moving_avg: Optional[torch.Tensor] = None # alpha_ma EMA snapshot
353
  attn_weights_pre_affine: Optional[torch.Tensor] = None # softmax weights before α,β
354
- attn_weights_post_affine: Optional[torch.Tensor] = None # α·softmax + β
355
 
356
  # ── Standard attention weights (eager non-affine path) ────────────
357
  attn_weights: Optional[torch.Tensor] = None # softmax weights (None for flash/sdpa)
@@ -1920,6 +1922,122 @@ def infer_key_validity(
1920
  return valid
1921
 
1922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1923
  def head_linear_compose(
1924
  hidden_states: torch.Tensor, mixing_matrix: torch.Tensor
1925
  ) -> torch.Tensor:
@@ -2061,21 +2179,38 @@ def affine_scaled_eager_attention_forward(
2061
  value_states = repeat_kv(value, module.num_key_value_groups)
2062
 
2063
  attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
 
 
 
 
 
 
 
2064
  if attention_mask is not None:
2065
- attn_weights = attn_weights + attention_mask[:, :, :, : key_states.shape[-2]]
 
 
 
 
 
 
2066
 
2067
  attn_weights_softmax = nn.functional.softmax(
2068
  attn_weights, dim=-1, dtype=torch.float32
2069
  ).to(query.dtype)
2070
 
 
 
 
2071
  if attn_analysis is not None:
2072
  attn_analysis.attn_weights_pre_affine = attn_weights_softmax.detach()
2073
 
2074
- # Affine reweighting: relaxes the unit-sum constraint of softmax.
2075
- # α ∈ [0, 1] scales down the softmax distribution (input-adaptively per head).
2076
- # β offsets to prevent collapse when α deviates from its running mean.
2077
- # Shapes: α, β are [B, H, S_q, 1], attn_weights is [B, H, S_q, S_k] → broadcast.
2078
  attn_weights_affine = alpha * attn_weights_softmax + beta
 
 
2079
 
2080
  if attn_analysis is not None:
2081
  attn_analysis.attn_weights_post_affine = attn_weights_affine.detach()
@@ -2101,7 +2236,7 @@ def affine_scaled_flash_attention_forward(
2101
  """
2102
  Affine-Scaled Attention — flash/sdpa path.
2103
 
2104
- Exact mathematical decomposition of [α·softmax(QKᵀ)+β]V using only the
2105
  public flash/sdpa interface — no kernel modification required.
2106
 
2107
  Derivation
@@ -2109,12 +2244,12 @@ def affine_scaled_flash_attention_forward(
2109
  The paper formula expands distributively:
2110
 
2111
  [α · softmax(QKᵀ/√dk) + β] · V
2112
- = α · [softmax(QKᵀ/√dk) · V] ← term 1: standard flash output
2113
- + β · [Σ_{j≤i} V_j] ← term 2: causal prefix-sum of V
2114
 
2115
- Term 2 follows because β is a scalar per query (broadcast over all S_k),
2116
- and softmax weights sum to 1 over the causal window:
2117
- β_i · Σ_j w_{i,j} · V_j = β_i · Σ_{j≤i} V_j (since Σ w_{i,j} = 1)
2118
 
2119
  Dropout
2120
  -------
@@ -2126,20 +2261,16 @@ def affine_scaled_flash_attention_forward(
2126
  regularisation that achieves the same intent without the intermediate
2127
  weight matrix. During inference dropout=0 so the paths are identical.
2128
 
2129
- Padding mask
2130
- ------------
2131
- The V_cumsum must not accumulate values from padding positions, since the
2132
- flash kernel zeros those out internally but a plain cumsum does not.
2133
- Depending on the backend contract, attention_mask can arrive as either a
2134
- 2D padding mask [B, S] (flash_attention_2/3) or a 4D additive mask
2135
- [B, 1, S, S] (eager/sdpa-style paths). We normalize both to a per-token
2136
- validity mask before the cumsum.
2137
 
2138
  Memory overhead vs standard flash call
2139
  ---------------------------------------
2140
- One extra tensor of shape [B, H_q, S, d_head] for V_cumsum.
2141
- At (B=1, H=8, S=2048, d_head=64, bf16): ≈ 4 MB per call, ≈ 48 MB total
2142
- across 12 layers. Allocated and freed within each forward call.
2143
 
2144
  Attention-weight analysis fields (attn_weights_pre_affine,
2145
  attn_weights_post_affine) remain None — those tensors are never
@@ -2161,28 +2292,18 @@ def affine_scaled_flash_attention_forward(
2161
  )
2162
  # flash_out: [B, S, H_q, d_head] — HF wrappers all return this layout
2163
 
2164
- # ── Term 2: β · causal prefix-sum of V ───────────────────────────────
2165
- # Expand V from KV heads to query heads for GQA.
2166
- value_expanded = repeat_kv(value, module.num_key_value_groups) # [B, H_q, S, d_head]
2167
-
2168
- # Zero out padded positions so they don't accumulate into the prefix sum.
2169
- # flash_attention_2/3 passes a 2D padding mask, while eager/sdpa-style
2170
- # paths may still pass a 4D additive mask.
2171
- if attention_mask is not None:
2172
- if attention_mask.ndim == 2:
2173
- valid = attention_mask.to(value_expanded.dtype) # [B, S]
2174
- valid = valid.unsqueeze(1).unsqueeze(-1) # [B, 1, S, 1]
2175
- value_expanded = value_expanded * valid # [B, H_q, S, d_head]
2176
- elif attention_mask.ndim == 4:
2177
- diag = attention_mask.diagonal(dim1=-2, dim2=-1) # [B, 1, S]
2178
- valid = (diag == 0).to(value_expanded.dtype) # [B, 1, S]
2179
- valid = valid.unsqueeze(-1) # [B, 1, S, 1]
2180
- value_expanded = value_expanded * valid # [B, H_q, S, d_head]
2181
-
2182
- # Causal prefix-sum: position i accumulates all valid j ≤ i.
2183
- v_cumsum = value_expanded.cumsum(dim=2) # [B, H_q, S, d_head]
2184
  # Transpose to match flash_out layout.
2185
- v_cumsum_t = v_cumsum.transpose(1, 2).contiguous() # [B, S, H_q, d_head]
2186
 
2187
  # α, β: [B, H_q, S, 1] → [B, S, H_q, 1] to broadcast over d_head.
2188
  alpha_t = alpha.permute(0, 2, 1, 3) # [B, S, H_q, 1]
@@ -3596,8 +3717,8 @@ class NeoLLMAttention(nn.Module):
3596
 
3597
  Calls flash_attn_func (dense path) or flash_attn_varlen_func (packed
3598
  path) directly. Affine-Scaled Attention is also computed inline here
3599
- so that both Term 1 (flash output) and Term 2 (V prefix-sum) operate
3600
- on the S·P expanded sequence consistently.
3601
 
3602
  Packing detection
3603
  -----------------
@@ -3614,12 +3735,11 @@ class NeoLLMAttention(nn.Module):
3614
  Affine-Scaled Attention (inline)
3615
  ---------------------------------
3616
  When use_affine=True:
3617
- output = α · flash_out + β · V_cumsum_t
3618
 
3619
  α, β are [B, H_q, S·P, 1] (already expanded over the seq dim before
3620
- this call). V_cumsum is the causal prefix-sum of V over the S·P
3621
- expanded sequence — semantically correct because virtual token (s, p)
3622
- accumulates all virtual tokens before it in interleaved order.
3623
 
3624
  Args
3625
  ----
@@ -3735,16 +3855,34 @@ class NeoLLMAttention(nn.Module):
3735
  ) # [B, S·P, H_q, d]
3736
 
3737
  # ── Affine-Scaled Attention inline (flash path) ───────────────────────
3738
- # Exact decomposition of [α·softmax(QKᵀ) + β]·V (Bae et al. 2026):
3739
  # Term 1: α · flash_out (standard flash output)
3740
- # Term 2: β · Σ_{j≤i} V_j (causal prefix-sum of V)
3741
- # Both terms operate on the S·P virtual sequence — semantically correct
3742
- # because token (s,p) causally precedes (s,p+1) and all of (s+1,·).
3743
  # α, β arrive pre-expanded to [B, H_q, S·P, 1]; permute to [B,S·P,H,1]
3744
  # to match the flash output layout [B, S·P, H_q, d].
3745
  if use_affine:
3746
  value_expanded = repeat_kv(v, self.num_key_value_groups) # [B,H_q,S·P,d]
3747
- v_cumsum_t = value_expanded.cumsum(dim=2).transpose(1, 2) # [B,S·P,H_q,d]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3748
  alpha_t = alpha.permute(0, 2, 1, 3) # [B,S·P,H_q,1]
3749
  beta_t = beta.permute(0, 2, 1, 3) # [B,S·P,H_q,1]
3750
  attn_out = alpha_t * flash_out + beta_t * v_cumsum_t # [B,S·P,H_q,d]
@@ -4095,7 +4233,7 @@ class NeoLLMAttention(nn.Module):
4095
  # Active whenever use_affine_scaled_attention=True, regardless of
4096
  # attention backend. Two code paths — same math, different execution:
4097
  # eager : full weight access, attn_weights_pre/post_affine captured.
4098
- # flash/sdpa: α·flash_out + β·V_cumsum, no weight tensors materialised.
4099
  alpha = None
4100
  beta = None
4101
  use_affine = self.use_affine_scaled_attention
@@ -4131,8 +4269,7 @@ class NeoLLMAttention(nn.Module):
4131
  if _iha_seq_expand:
4132
  alpha = alpha.repeat_interleave(self.iha_P, dim=2) # [B,H_q,S·P,1]
4133
  beta = beta.repeat_interleave(self.iha_P, dim=2) # [B,H_q,S·P,1]
4134
- # Recompute beta with the correct N = S·P (denominator scales
4135
- # with the number of virtual keys the query can attend to).
4136
  # alpha_ma shape [1,H,1,1] broadcasts correctly against expanded alpha.
4137
  N_expanded = k.shape[-2] # S·P after IHA expansion
4138
  beta = (self.alpha_ma.to(alpha.dtype) - alpha) / max(N_expanded, 1)
@@ -4147,7 +4284,7 @@ class NeoLLMAttention(nn.Module):
4147
  # 3. attention_mask shaped [B,S] is incompatible with S·P tokens
4148
  # _iha_seq_expand_flash_forward calls flash_attn_func /
4149
  # flash_attn_varlen_func directly, handling packing, sliding window,
4150
- # and the Affine-Scaled inline decomposition (α·flash + β·V_cumsum).
4151
  # alpha/beta are [B, H_q, S·P, 1] at this point (already expanded).
4152
  attn_out, attn_weights = self._iha_seq_expand_flash_forward(
4153
  q, k, v,
@@ -4169,7 +4306,7 @@ class NeoLLMAttention(nn.Module):
4169
  **kwargs,
4170
  )
4171
  else:
4172
- # Flash / SDPA: exact formula via V.cumsum, no weight tensors.
4173
  # attn_weights_pre/post_affine remain None in AnalysisState.
4174
  backend_kwargs = kwargs
4175
  if flash_local_sliding_window is not None:
 
26
  increases attention entropy, and is complementary to Gated Attention
27
  (Bae et al. 2026, Table 2: Affine-Scaled + Gated > either alone).
28
 
29
+ Flash/SDPA path:
30
  Expanding [α·softmax(QKᵀ)+β]V distributively yields two terms:
31
+ α · attention(Q,K,V) — backend computes this directly
32
+ + β · Σ_{j∈A(i)} V_j — sum over the same valid keys A(i)
33
+ For global causal attention A(i) is a prefix and this is V.cumsum.
34
+ For sliding-window or packed sequences the sum is windowed or
35
+ segmented so β cannot leak outside the backend attention mask.
36
+ Per-weight tensors (attn_weights_pre/post_affine) are unavailable
37
+ in flash mode and remain None in AnalysisState.
38
 
39
+ Eager path: applies the same valid-key mask to α·softmax + β and keeps
40
+ full weight access for interpretability.
41
 
42
  References:
43
  FANformer: "FANformer: Improving Large Language Models Through Effective
 
88
  from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
89
  from transformers.processing_utils import Unpack
90
  from transformers.utils import TransformersKwargs, logging
91
+ from configuration_neollm import NeoLLMConfig
92
 
93
  from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
94
  torch._dynamo.config.capture_scalar_outputs = True
 
353
  beta_per_head: Optional[torch.Tensor] = None # β [B,H,S,1] moving-avg bias
354
  alpha_moving_avg: Optional[torch.Tensor] = None # alpha_ma EMA snapshot
355
  attn_weights_pre_affine: Optional[torch.Tensor] = None # softmax weights before α,β
356
+ attn_weights_post_affine: Optional[torch.Tensor] = None # valid_mask·(α·softmax + β)
357
 
358
  # ── Standard attention weights (eager non-affine path) ────────────
359
  attn_weights: Optional[torch.Tensor] = None # softmax weights (None for flash/sdpa)
 
1922
  return valid
1923
 
1924
 
1925
+
1926
+
1927
+ def _expand_pair_valid_mask(
1928
+ attention_mask: Optional[torch.Tensor],
1929
+ batch_size: int,
1930
+ num_heads: int,
1931
+ query_len: int,
1932
+ key_len: int,
1933
+ ) -> Optional[torch.Tensor]:
1934
+ """
1935
+ Return a boolean [B,H,Sq,Sk] mask for positions that attention may read.
1936
+
1937
+ Supported contracts used in this file:
1938
+ • 2D padding mask [B,Sk], with truthy values for valid keys.
1939
+ • 4D additive mask [B,1|H,Sq,Sk], with 0 on valid pairs and -inf/large
1940
+ negative values on invalid causal, local or padding pairs.
1941
+ """
1942
+ if attention_mask is None:
1943
+ return None
1944
+
1945
+ if attention_mask.ndim == 2:
1946
+ if attention_mask.shape[-1] < key_len:
1947
+ return None
1948
+ valid = attention_mask[:, :key_len].to(dtype=torch.bool) # [B,Sk]
1949
+ valid = valid[:, None, None, :].expand(batch_size, num_heads, query_len, key_len)
1950
+ return valid
1951
+
1952
+ if attention_mask.ndim != 4:
1953
+ return None
1954
+ if attention_mask.shape[-2] < query_len or attention_mask.shape[-1] < key_len:
1955
+ return None
1956
+
1957
+ valid = attention_mask[:, :, :query_len, :key_len] == 0
1958
+ if valid.shape[1] == 1 and num_heads != 1:
1959
+ valid = valid.expand(-1, num_heads, -1, -1)
1960
+ elif valid.shape[1] != num_heads:
1961
+ valid = valid[:, :1, :, :].expand(-1, num_heads, -1, -1)
1962
+ return valid
1963
+
1964
+
1965
+ def _masked_value_sum_from_pairs(
1966
+ value_expanded: torch.Tensor,
1967
+ pair_valid: torch.Tensor,
1968
+ ) -> torch.Tensor:
1969
+ """Compute Σ_{j∈A(i)} V_j from an explicit pair-valid mask."""
1970
+ return torch.matmul(pair_valid.to(value_expanded.dtype), value_expanded)
1971
+
1972
+
1973
+ def _causal_value_sum(
1974
+ value_expanded: torch.Tensor,
1975
+ query_len: int,
1976
+ window_left: Optional[int] = None,
1977
+ ) -> torch.Tensor:
1978
+ """
1979
+ Compute Σ_{j∈A(i)} V_j for causal or sliding-window causal attention.
1980
+
1981
+ value_expanded: [B,H,Sk,d]. The returned tensor is [B,H,Sq,d].
1982
+ For cached decoding, Sq may be smaller than Sk; query rows are aligned to
1983
+ the last Sq key positions, matching causal self-attention with cache.
1984
+ """
1985
+ key_len = value_expanded.shape[2]
1986
+ prefix = value_expanded.cumsum(dim=2) # [B,H,Sk,d]
1987
+
1988
+ if window_left is None or window_left < 0 or window_left >= key_len:
1989
+ sums_all = prefix
1990
+ else:
1991
+ # Valid keys for absolute position i are max(0, i-window_left) ... i.
1992
+ prefix_padded = F.pad(prefix, (0, 0, 1, 0)) # [B,H,Sk+1,d]
1993
+ end_idx = torch.arange(key_len, device=value_expanded.device) + 1
1994
+ start_idx = torch.clamp(end_idx - (int(window_left) + 1), min=0)
1995
+ end_vals = prefix_padded.index_select(2, end_idx)
1996
+ start_vals = prefix_padded.index_select(2, start_idx)
1997
+ sums_all = end_vals - start_vals
1998
+
1999
+ if query_len == key_len:
2000
+ return sums_all
2001
+ start = max(key_len - query_len, 0)
2002
+ return sums_all[:, :, start:start + query_len, :]
2003
+
2004
+
2005
+ def _affine_valid_value_sum(
2006
+ value_expanded: torch.Tensor,
2007
+ attention_mask: Optional[torch.Tensor],
2008
+ query_len: int,
2009
+ sliding_window: Optional[Union[int, Tuple[int, int]]] = None,
2010
+ ) -> torch.Tensor:
2011
+ """
2012
+ Compute the β term support S_i = Σ_{j∈A(i)} V_j for Affine attention.
2013
+
2014
+ It first respects an explicit 4D additive mask exactly. Otherwise it uses
2015
+ causal prefix sums, optionally restricted by a left sliding-window size.
2016
+ """
2017
+ batch_size, num_heads, key_len, _ = value_expanded.shape
2018
+
2019
+ pair_valid = None
2020
+ if attention_mask is not None and attention_mask.ndim == 4:
2021
+ pair_valid = _expand_pair_valid_mask(
2022
+ attention_mask, batch_size, num_heads, query_len, key_len
2023
+ )
2024
+ if pair_valid is not None:
2025
+ return _masked_value_sum_from_pairs(value_expanded, pair_valid)
2026
+
2027
+ # 2D padding masks zero invalid keys before the prefix/window sum.
2028
+ if attention_mask is not None and attention_mask.ndim == 2:
2029
+ if attention_mask.shape[-1] >= key_len:
2030
+ valid = attention_mask[:, :key_len].to(value_expanded.dtype)
2031
+ value_expanded = value_expanded * valid[:, None, :, None]
2032
+
2033
+ window_left = None
2034
+ if isinstance(sliding_window, tuple):
2035
+ window_left = int(sliding_window[0]) if len(sliding_window) > 0 else None
2036
+ elif sliding_window is not None:
2037
+ window_left = int(sliding_window)
2038
+
2039
+ return _causal_value_sum(value_expanded, query_len, window_left)
2040
+
2041
  def head_linear_compose(
2042
  hidden_states: torch.Tensor, mixing_matrix: torch.Tensor
2043
  ) -> torch.Tensor:
 
2179
  value_states = repeat_kv(value, module.num_key_value_groups)
2180
 
2181
  attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
2182
+ pair_valid = _expand_pair_valid_mask(
2183
+ attention_mask,
2184
+ batch_size=query.shape[0],
2185
+ num_heads=key_states.shape[1],
2186
+ query_len=query.shape[-2],
2187
+ key_len=key_states.shape[-2],
2188
+ )
2189
  if attention_mask is not None:
2190
+ if attention_mask.ndim == 4:
2191
+ attn_weights = attn_weights + attention_mask[:, :, : query.shape[-2], : key_states.shape[-2]]
2192
+ elif attention_mask.ndim == 2:
2193
+ key_valid = attention_mask[:, : key_states.shape[-2]].to(dtype=torch.bool)
2194
+ additive = torch.zeros_like(attn_weights)
2195
+ additive = additive.masked_fill(~key_valid[:, None, None, :], torch.finfo(attn_weights.dtype).min)
2196
+ attn_weights = attn_weights + additive
2197
 
2198
  attn_weights_softmax = nn.functional.softmax(
2199
  attn_weights, dim=-1, dtype=torch.float32
2200
  ).to(query.dtype)
2201
 
2202
+ if pair_valid is not None:
2203
+ attn_weights_softmax = attn_weights_softmax.masked_fill(~pair_valid, 0.0)
2204
+
2205
  if attn_analysis is not None:
2206
  attn_analysis.attn_weights_pre_affine = attn_weights_softmax.detach()
2207
 
2208
+ # Affine reweighting over valid keys only. β must obey the same causal,
2209
+ # local and padding mask as softmax; otherwise invalid V_j would leak in.
2210
+ # Shapes: α, β are [B, H, S_q, 1], weights are [B, H, S_q, S_k].
 
2211
  attn_weights_affine = alpha * attn_weights_softmax + beta
2212
+ if pair_valid is not None:
2213
+ attn_weights_affine = attn_weights_affine.masked_fill(~pair_valid, 0.0)
2214
 
2215
  if attn_analysis is not None:
2216
  attn_analysis.attn_weights_post_affine = attn_weights_affine.detach()
 
2236
  """
2237
  Affine-Scaled Attention — flash/sdpa path.
2238
 
2239
+ Mathematical decomposition of [α·softmax(QKᵀ)+β]V using only the
2240
  public flash/sdpa interface — no kernel modification required.
2241
 
2242
  Derivation
 
2244
  The paper formula expands distributively:
2245
 
2246
  [α · softmax(QKᵀ/√dk) + β] · V
2247
+ = α · [softmax(QKᵀ/√dk) · V] ← term 1: standard backend output
2248
+ + β · [Σ_{j∈A(i)} V_j] ← term 2: same valid keys A(i)
2249
 
2250
+ For global causal attention, A(i) is the prefix j≤i and term 2 is a
2251
+ cumsum. For sliding-window or packed/additive-mask paths, term 2 must use
2252
+ the corresponding windowed or explicit valid-key sum.
2253
 
2254
  Dropout
2255
  -------
 
2261
  regularisation that achieves the same intent without the intermediate
2262
  weight matrix. During inference dropout=0 so the paths are identical.
2263
 
2264
+ Mask handling
2265
+ -------------
2266
+ The β value-sum must respect the same valid keys as the backend attention.
2267
+ A 2D padding mask zeros invalid keys before prefix/window sums. A 4D
2268
+ additive mask is used directly to compute Σ_{j∈A(i)} V_j exactly.
 
 
 
2269
 
2270
  Memory overhead vs standard flash call
2271
  ---------------------------------------
2272
+ Global causal attention adds one [B, H_q, S, d_head] value-sum tensor.
2273
+ Exact 4D-mask fallback may materialise a [B, H_q, S_q, S_k] boolean mask.
 
2274
 
2275
  Attention-weight analysis fields (attn_weights_pre_affine,
2276
  attn_weights_post_affine) remain None — those tensors are never
 
2292
  )
2293
  # flash_out: [B, S, H_q, d_head] — HF wrappers all return this layout
2294
 
2295
+ # ── Term 2: β · Σ_{j∈A(i)} V_j ──────────────────────────────────────
2296
+ # Expand V from KV heads to query heads for GQA, then sum only over the
2297
+ # same valid keys used by the backend attention path.
2298
+ value_expanded = repeat_kv(value, module.num_key_value_groups) # [B, H_q, S_k, d_head]
2299
+ value_sum = _affine_valid_value_sum(
2300
+ value_expanded,
2301
+ attention_mask=attention_mask,
2302
+ query_len=query.shape[-2],
2303
+ sliding_window=kwargs.get("sliding_window", None),
2304
+ ) # [B, H_q, S_q, d_head]
 
 
 
 
 
 
 
 
 
 
2305
  # Transpose to match flash_out layout.
2306
+ v_cumsum_t = value_sum.transpose(1, 2).contiguous() # [B, S_q, H_q, d_head]
2307
 
2308
  # α, β: [B, H_q, S, 1] → [B, S, H_q, 1] to broadcast over d_head.
2309
  alpha_t = alpha.permute(0, 2, 1, 3) # [B, S, H_q, 1]
 
3717
 
3718
  Calls flash_attn_func (dense path) or flash_attn_varlen_func (packed
3719
  path) directly. Affine-Scaled Attention is also computed inline here
3720
+ so that Term 2 sums V over the same valid keys as flash attention on
3721
+ the S·P expanded sequence.
3722
 
3723
  Packing detection
3724
  -----------------
 
3735
  Affine-Scaled Attention (inline)
3736
  ---------------------------------
3737
  When use_affine=True:
3738
+ output = α · flash_out + β · Σ_{j∈A(i)} V_j
3739
 
3740
  α, β are [B, H_q, S·P, 1] (already expanded over the seq dim before
3741
+ this call). The V sum is causal prefix-sum for global IHA, windowed
3742
+ for local IHA, and segmented when packed position_ids reset.
 
3743
 
3744
  Args
3745
  ----
 
3855
  ) # [B, S·P, H_q, d]
3856
 
3857
  # ── Affine-Scaled Attention inline (flash path) ───────────────────────
3858
+ # Decomposition of [α·softmax(QKᵀ) + β]·V (Bae et al. 2026):
3859
  # Term 1: α · flash_out (standard flash output)
3860
+ # Term 2: β · Σ_{j∈A(i)} V_j (same valid keys as flash)
3861
+ # For local IHA, A(i) is the flash sliding window; for packed IHA it is
3862
+ # additionally reset at each document boundary.
3863
  # α, β arrive pre-expanded to [B, H_q, S·P, 1]; permute to [B,S·P,H,1]
3864
  # to match the flash output layout [B, S·P, H_q, d].
3865
  if use_affine:
3866
  value_expanded = repeat_kv(v, self.num_key_value_groups) # [B,H_q,S·P,d]
3867
+ window_left = window[0] if isinstance(window, tuple) else None
3868
+ if is_packed:
3869
+ value_sum = torch.empty_like(value_expanded)
3870
+ start = 0
3871
+ for _seg_len in doc_lengths_p.to(dtype=torch.long).tolist():
3872
+ end = start + int(_seg_len)
3873
+ value_sum[:, :, start:end, :] = _causal_value_sum(
3874
+ value_expanded[:, :, start:end, :],
3875
+ query_len=int(_seg_len),
3876
+ window_left=window_left,
3877
+ )
3878
+ start = end
3879
+ else:
3880
+ value_sum = _causal_value_sum(
3881
+ value_expanded,
3882
+ query_len=SP,
3883
+ window_left=window_left,
3884
+ )
3885
+ v_cumsum_t = value_sum.transpose(1, 2).contiguous() # [B,S·P,H_q,d]
3886
  alpha_t = alpha.permute(0, 2, 1, 3) # [B,S·P,H_q,1]
3887
  beta_t = beta.permute(0, 2, 1, 3) # [B,S·P,H_q,1]
3888
  attn_out = alpha_t * flash_out + beta_t * v_cumsum_t # [B,S·P,H_q,d]
 
4233
  # Active whenever use_affine_scaled_attention=True, regardless of
4234
  # attention backend. Two code paths — same math, different execution:
4235
  # eager : full weight access, attn_weights_pre/post_affine captured.
4236
+ # flash/sdpa: α·backend_out + β·Σ valid V, no weight tensors materialised.
4237
  alpha = None
4238
  beta = None
4239
  use_affine = self.use_affine_scaled_attention
 
4269
  if _iha_seq_expand:
4270
  alpha = alpha.repeat_interleave(self.iha_P, dim=2) # [B,H_q,S·P,1]
4271
  beta = beta.repeat_interleave(self.iha_P, dim=2) # [B,H_q,S·P,1]
4272
+ # Recompute beta with N = S·P for the expanded attention axis.
 
4273
  # alpha_ma shape [1,H,1,1] broadcasts correctly against expanded alpha.
4274
  N_expanded = k.shape[-2] # S·P after IHA expansion
4275
  beta = (self.alpha_ma.to(alpha.dtype) - alpha) / max(N_expanded, 1)
 
4284
  # 3. attention_mask shaped [B,S] is incompatible with S·P tokens
4285
  # _iha_seq_expand_flash_forward calls flash_attn_func /
4286
  # flash_attn_varlen_func directly, handling packing, sliding window,
4287
+ # and the Affine-Scaled inline decomposition (α·flash + β·Σ valid V).
4288
  # alpha/beta are [B, H_q, S·P, 1] at this point (already expanded).
4289
  attn_out, attn_weights = self._iha_seq_expand_flash_forward(
4290
  q, k, v,
 
4306
  **kwargs,
4307
  )
4308
  else:
4309
+ # Flash / SDPA: valid-key value sum, no weight tensors.
4310
  # attn_weights_pre/post_affine remain None in AnalysisState.
4311
  backend_kwargs = kwargs
4312
  if flash_local_sliding_window is not None: