@@ -3088,10 +3088,16 @@ def _find_mamba_match_count(
30883088
30893089 mamba_map = self .mamba_slot_allocator .hash_to_block_id
30903090 hashes = req .precomputed_block_hashes [start_block :end_block ]
3091- for i in range (len (hashes ) - 1 , - 1 , - 1 ):
3092- if hashes [i ] in mamba_map :
3093- return i + 1
3094- return 0
3091+
3092+ # Mark the blocks in range whose hash the allocator still holds state
3093+ # for; the farthest such block is the match count. Intersecting against
3094+ # the range's hashes first keeps this bounded by the range rather than
3095+ # the size of the whole cache.
3096+ block_hashes = torch .tensor (hashes , dtype = torch .int64 )
3097+ cached = mamba_map .keys () & set (hashes )
3098+ cached_hashes = torch .tensor (list (cached ), dtype = torch .int64 )
3099+ is_cached = torch .isin (block_hashes , cached_hashes )
3100+ return int (is_cached .nonzero ()[- 1 ].item ()) + 1 if is_cached .any () else 0
30953101
30963102 def _compute_prefix_match (
30973103 self ,
@@ -3148,7 +3154,9 @@ def _compute_prefix_match(
31483154 # already had Mamba state restored during the first chunk.
31493155 if self .is_hybrid_model and self .mamba_slot_allocator is not None and finished == 0 :
31503156 num_mamba_matched = self ._find_mamba_match_count (
3151- req , already_allocated_blocks , already_allocated_blocks + num_matched
3157+ req = req ,
3158+ start_block = already_allocated_blocks ,
3159+ end_block = already_allocated_blocks + num_matched ,
31523160 )
31533161 if record_mamba_match :
31543162 req ._mamba_num_matched_blocks = num_mamba_matched
@@ -3159,12 +3167,9 @@ def _compute_prefix_match(
31593167 raw_skip = num_mamba_matched * self .block_size_tokens
31603168 if raw_skip >= prefill_chunk_length :
31613169 # Back off to previous block with cached Mamba state
3162- mamba_map = self .mamba_slot_allocator .hash_to_block_id
3163- backed_off_blocks = 0
3164- for j in range (num_mamba_matched - 2 , - 1 , - 1 ):
3165- if req .precomputed_block_hashes [j ] in mamba_map :
3166- backed_off_blocks = j + 1
3167- break
3170+ backed_off_blocks = self ._find_mamba_match_count (
3171+ req = req , start_block = 0 , end_block = num_mamba_matched - 1
3172+ )
31683173 prefix_skip_tokens = backed_off_blocks * self .block_size_tokens
31693174 else :
31703175 prefix_skip_tokens = raw_skip
@@ -3183,6 +3188,32 @@ def _compute_prefix_match(
31833188 max_skip = prefill_chunk_length - 2
31843189 prefix_skip_tokens = (max_skip // self .block_size_tokens ) * self .block_size_tokens
31853190
3191+ # Rounding down can land on a block that has no cached Mamba state.
3192+ # add_request() restores from `prefix_skip_tokens // block_size - 1`
3193+ # unconditionally and, when `restore_to_live` misses, ZEROES the SSM
3194+ # state while still skipping the tokens -- the request then resumes
3195+ # mid-prompt from a zero state and produces a wrong (but internally
3196+ # coherent) distribution for its first generated token.
3197+ #
3198+ # Mamba boundaries are sparse: only the few positions selected in
3199+ # `compute_and_store_offsets` are cached, so the clamped boundary is
3200+ # frequently not one of them. A 5889-token prompt caches state only at
3201+ # block 22 (offset 5888), the clamp moves the skip to 5632, and the
3202+ # restore then targets block 21, which has none.
3203+ #
3204+ # Walk back to the nearest block that actually has cached state, the
3205+ # same way the `raw_skip >= prefill_chunk_length` branch above does.
3206+ if (
3207+ self .is_hybrid_model
3208+ and self .mamba_slot_allocator is not None
3209+ and finished == 0
3210+ and prefix_skip_tokens > 0
3211+ ):
3212+ usable = self ._find_mamba_match_count (
3213+ req = req , start_block = 0 , end_block = prefix_skip_tokens // self .block_size_tokens
3214+ )
3215+ prefix_skip_tokens = usable * self .block_size_tokens
3216+
31863217 effective_prefill_chunk_length = prefill_chunk_length - prefix_skip_tokens
31873218 num_blocks_from_pool = max (
31883219 0 , overall_required_blocks - already_allocated_blocks - num_matched
0 commit comments