1818 _create_causal_mask_for_ring_buffer ,
1919 AttentionMHA ,
2020 KVCache ,
21- RingKVCache ,
2221)
2322from executorch .examples .models .llama .model_args import ModelArgs
2423from executorch .examples .models .llama .rope import Rope
2524from torchao .quantization .quant_api import _replace_with_custom_fn_if_matches_filter
2625
2726
27+ def _get_attention_sink_cache_size (
28+ max_context_length : int ,
29+ window_size : int ,
30+ sink_size : int ,
31+ max_seq_len : Optional [int ] = None ,
32+ ) -> int :
33+ """Size a sink cache for fixed sinks, one window, and one input chunk."""
34+ assert sink_size >= 0 , "Attention sink size must be non-negative"
35+ assert sink_size < max_context_length , (
36+ f"Attention sink size ({ sink_size } ) must be smaller than the full "
37+ f"context length ({ max_context_length } )"
38+ )
39+ assert window_size > 0 , "Sliding-window size must be positive"
40+ assert sink_size + window_size <= max_context_length , (
41+ f"Attention sink size ({ sink_size } ) plus sliding-window size "
42+ f"({ window_size } ) cannot exceed the full context length "
43+ f"({ max_context_length } )"
44+ )
45+ if max_seq_len is None :
46+ max_seq_len = max_context_length
47+ assert max_seq_len > 0 , "Maximum sequence length must be positive"
48+ return sink_size + window_size + max_seq_len
49+
50+
2851class RopeWithAttentionSink (Rope ):
2952 """
3053 Rope subclass for Attention Sink models.
@@ -37,10 +60,11 @@ class RopeWithAttentionSink(Rope):
3760 - Window tokens (pos >= sink_size): wrapped into ring buffer range
3861 [sink_size, sink_size + ring_size) via modulo
3962
40- The ring buffer is 2x window_size for write-ahead headroom, not to keep the
41- live window contiguous -- it can span a wrap. Across a wrap two positions
42- remap to a difference that is not their true distance, so RoPE preserves
43- relative distance only within a wrap.
63+ The ring buffer holds one retained window plus one maximum-size input
64+ chunk. It is larger than the live window for write-ahead headroom, not to
65+ keep the live window contiguous -- it can span a wrap. Across a wrap two
66+ positions remap to a difference that is not their true distance, so RoPE
67+ preserves relative distance only within a wrap.
4468 """
4569
4670 def __init__ (
@@ -52,7 +76,26 @@ def __init__(
5276 super ().__init__ (params )
5377 self .window_size = window_size
5478 self .sink_size = sink_size
55- self .ring_size = window_size * 2
79+ max_seq_len = (
80+ params .max_context_len
81+ if getattr (params , "max_seq_len" , None ) is None
82+ else int (params .max_seq_len )
83+ )
84+ cache_size = _get_attention_sink_cache_size (
85+ params .max_context_len ,
86+ window_size ,
87+ sink_size ,
88+ max_seq_len ,
89+ )
90+ self .ring_size = cache_size - sink_size
91+ if self .freqs_cos .size (0 ) < cache_size :
92+ freqs_cos , freqs_sin = self .precompute_freqs_cis (
93+ self .params .head_dim ,
94+ cache_size ,
95+ self .params .rope_freq_base ,
96+ )
97+ self .freqs_cos = freqs_cos
98+ self .freqs_sin = freqs_sin
5699
57100 def _remap_input_pos (self , input_pos : torch .Tensor ) -> torch .Tensor :
58101 """Remap positions: sink tokens stay, window tokens wrap in ring buffer."""
@@ -133,7 +176,7 @@ class CachePositionsManagerWithSink(nn.Module):
133176 For sink_size=0: behaves exactly like original CachePositionsManager.
134177 For sink_size>0: sink tokens go to fixed positions, rest uses ring buffer.
135178
136- IMPORTANT: cache_size should be the actual cache dimension size (2x window for ring buffer) .
179+ IMPORTANT: cache_size is the actual cache dimension, including sink slots .
137180 """
138181
139182 def __init__ (self , cache_size : int , sink_size : int = 0 ):
@@ -191,7 +234,7 @@ class KVCacheWithAttentionSink(KVCache):
191234 Uses a ring buffer approach for the sliding window portion while keeping
192235 the first sink_size tokens fixed. This avoids dynamic shape operations.
193236
194- Cache layout: [sink: 0 to sink_size-1 ] [ring_buffer: sink_size to sink_size + window_size*2 - 1 ]
237+ Cache layout: [fixed sink tokens ] [ring buffer for one window + one chunk ]
195238 """
196239
197240 def __init__ (
@@ -202,16 +245,27 @@ def __init__(
202245 rope : RopeWithAttentionSink ,
203246 window_size : int ,
204247 sink_size : int ,
248+ max_context_length : int ,
249+ max_seq_len : Optional [int ] = None ,
205250 max_batch_size : int = 1 ,
206251 dtype = torch .float32 ,
207252 ):
208- # Total cache size is sink_size + window_size * 2.
209- # The ring buffer needs 2x the window size because at the moment a new
210- # token is written, the previous window_size tokens must still be readable
211- # (they haven't been overwritten yet). With only 1x, writing a new entry
212- # would immediately evict the oldest visible token, leaving fewer than
213- # window_size tokens available for attention.
214- total_cache_size = sink_size + window_size * 2
253+ self .full_context_length = max_context_length
254+ self .max_seq_len = (
255+ max_context_length if max_seq_len is None else int (max_seq_len )
256+ )
257+ # Keep the fixed sinks separate from the ring space needed for the
258+ # retained window and the largest in-flight input chunk.
259+ total_cache_size = _get_attention_sink_cache_size (
260+ max_context_length ,
261+ window_size ,
262+ sink_size ,
263+ self .max_seq_len ,
264+ )
265+ assert rope .ring_size == total_cache_size - sink_size , (
266+ f"RoPE ring size ({ rope .ring_size } ) must match the KV-cache ring "
267+ f"size ({ total_cache_size - sink_size } )"
268+ )
215269 super ().__init__ (
216270 max_batch_size = max_batch_size ,
217271 max_context_length = total_cache_size ,
@@ -310,6 +364,7 @@ def _replace_attention(
310364 rope_with_attention_sink : RopeWithAttentionSink ,
311365 sink_size : int ,
312366 window_size : int ,
367+ max_seq_len : Optional [int ],
313368):
314369 for _ , child_module in module ._modules .items ():
315370 if len (list (child_module .children ())) > 0 : # pyre-ignore [16]
@@ -318,32 +373,23 @@ def _replace_attention(
318373 rope_with_attention_sink = rope_with_attention_sink ,
319374 sink_size = sink_size ,
320375 window_size = window_size ,
376+ max_seq_len = max_seq_len ,
321377 )
322378
323379 if isinstance (child_module , AttentionMHA ):
324380 kv_cache = child_module .kv_cache
325- if sink_size == 0 :
326- # No sink tokens needed — use standard RingKVCache directly
327- child_module .kv_cache = RingKVCache (
328- kv_cache .max_batch_size ,
329- window_size , # RingKVCache expects user-provided window size
330- kv_cache .n_heads ,
331- kv_cache .head_dim ,
332- kv_cache .enable_dynamic_shape ,
333- kv_cache .k_cache .dtype ,
334- )
335- else :
336- kv_cache_with_attention_sink = KVCacheWithAttentionSink (
337- n_heads = kv_cache .n_heads ,
338- head_dim = kv_cache .head_dim ,
339- enable_dynamic_shape = kv_cache .enable_dynamic_shape ,
340- rope = rope_with_attention_sink ,
341- max_batch_size = kv_cache .max_batch_size ,
342- window_size = window_size ,
343- sink_size = sink_size ,
344- dtype = kv_cache .k_cache .dtype ,
345- )
346- child_module .kv_cache = kv_cache_with_attention_sink
381+ child_module .kv_cache = KVCacheWithAttentionSink (
382+ n_heads = kv_cache .n_heads ,
383+ head_dim = kv_cache .head_dim ,
384+ enable_dynamic_shape = kv_cache .enable_dynamic_shape ,
385+ rope = rope_with_attention_sink ,
386+ max_batch_size = kv_cache .max_batch_size ,
387+ window_size = window_size ,
388+ sink_size = sink_size ,
389+ max_context_length = kv_cache .max_context_length ,
390+ max_seq_len = max_seq_len ,
391+ dtype = kv_cache .k_cache .dtype ,
392+ )
347393 # Don't replace forward - let the original AttentionMHA.forward handle it
348394 # since our KVCache has is_ring_buffer=True, it will use the ring buffer mask
349395
@@ -365,11 +411,13 @@ def enable_attention_sink(
365411 window_size = window_size ,
366412 sink_size = sink_size ,
367413 )
414+ max_seq_len = getattr (params , "max_seq_len" , None )
368415 _replace_rope (module , rope_with_attention_sink )
369416 _replace_attention (
370417 module = module ,
371418 rope_with_attention_sink = rope_with_attention_sink ,
372419 sink_size = sink_size ,
373420 window_size = window_size ,
421+ max_seq_len = max_seq_len ,
374422 )
375423 return module
0 commit comments