@@ -261,6 +261,80 @@ def test_hybrid_mamba_align_partial_hash_hit():
261261 assert manager .get_blocks ("1" ).blocks [1 ][1 ].block_hash_num_tokens == 8
262262
263263
264+ def test_eagle_group_registers_unaligned_tail_under_partial_hash_hits ():
265+ """An EAGLE group must not re-floor what partial hash hits leaves un-floored.
266+
267+ ``cache_blocks`` decides once how far a request may be registered, and with
268+ fine-grained partial hash hits that bound is the raw token count. The EAGLE
269+ branch then re-derives its own bound for the lookahead block; if it rounds
270+ down to ``scheduler_block_size`` again, everything between the last aligned
271+ boundary and the tail stops being registered -- ``(n % scheduler_block_size)
272+ - manager.block_size`` tokens per call, which is most of a segment whenever
273+ the group's own block is much smaller than the scheduler block.
274+ """
275+ hash_block_size = 2
276+ mamba_block_size = 4 * hash_block_size
277+ kv_cache_config = KVCacheConfig (
278+ num_blocks = 40 ,
279+ kv_cache_tensors = [],
280+ kv_cache_groups = [
281+ KVCacheGroupSpec (
282+ ["full" ],
283+ FullAttentionSpec (
284+ block_size = hash_block_size ,
285+ num_kv_heads = 1 ,
286+ head_size = 1 ,
287+ dtype = torch .float32 ,
288+ ),
289+ ),
290+ KVCacheGroupSpec (
291+ ["mamba" ],
292+ MambaSpec (
293+ block_size = mamba_block_size ,
294+ shapes = (1 , 1 ),
295+ dtypes = (torch .float32 ,),
296+ mamba_cache_mode = "align" ,
297+ ),
298+ ),
299+ ],
300+ )
301+ manager = make_kv_cache_manager (
302+ kv_cache_config = kv_cache_config ,
303+ max_model_len = 8192 ,
304+ enable_caching = True ,
305+ hash_block_size = hash_block_size ,
306+ )
307+ coordinator = manager .coordinator
308+ assert coordinator .enable_partial_hash_hits
309+ # The full-attention group is the EAGLE one, and its block is smaller than
310+ # the scheduler block -- the geometry where re-flooring loses tokens.
311+ eagle_manager = coordinator .single_type_managers [0 ]
312+ eagle_manager .use_eagle = True
313+ assert eagle_manager .block_size < coordinator .scheduler_block_size
314+
315+ # Deliberately not a multiple of the scheduler block, so the two bounds
316+ # differ: floor(22/8)*8 + 2 = 18 against 22.
317+ num_tokens = coordinator .scheduler_block_size * 2 + hash_block_size * 3
318+ req = make_request ("0" , list (range (num_tokens )), hash_block_size , sha256 )
319+
320+ recorded : list [int ] = []
321+ for single_type_manager in coordinator .single_type_managers :
322+ original = single_type_manager .cache_blocks
323+
324+ def spy (request , num_tokens_to_cache , * args , _orig = original , ** kwargs ):
325+ recorded .append (num_tokens_to_cache )
326+ return _orig (request , num_tokens_to_cache , * args , ** kwargs )
327+
328+ single_type_manager .cache_blocks = spy
329+
330+ # allocate_slots caches on the way out, so this exercises the real path.
331+ computed_blocks , num_computed , _ = manager .get_computed_blocks (req )
332+ assert manager .allocate_slots (req , num_tokens , num_computed , computed_blocks )
333+
334+ # Every group, EAGLE or not, may register the whole unaligned tail.
335+ assert recorded == [num_tokens ] * len (coordinator .single_type_managers )
336+
337+
264338def test_hybrid_mamba_partial_tail_owner_uses_cow_on_continue ():
265339 hash_block_size = 2
266340 block_size = 2 * hash_block_size
0 commit comments