Skip to content

Commit 47f079a

Browse files
authored
Fix Mamba prefix cache clamp for < 2 token chunk edge case (#6930)
Signed-off-by: Keshav Santhanam <ksanthanam@nvidia.com>
1 parent 6ffe9f7 commit 47f079a

2 files changed

Lines changed: 73 additions & 11 deletions

File tree

megatron/core/inference/contexts/dynamic_context.py

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/unit_tests/inference/contexts/test_dynamic_prefix_caching.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,37 @@ def test_mamba_prefill_skip_and_zero_prefill(self):
10461046
ctx5.release_memory_blocks_from_request_indexes([0])
10471047
assert not msa5.has_state(bid5) and bh5 not in msa5.hash_to_block_id
10481048

1049+
@pytest.mark.internal
1050+
def test_mamba_prefill_skip_clamp_lands_on_cached_block(self):
1051+
# The clamp that keeps effective_prefill_chunk_length >= 2 rounds the skip
1052+
# down to a block boundary, which can land on a block that has no cached
1053+
# Mamba state. The skip must walk back to the nearest block that does,
1054+
# otherwise add_request() zeroes the SSM state and resumes mid-prompt.
1055+
# One token past 3 full blocks: skipping all 3 leaves a 1-token chunk, so
1056+
# the clamp always fires and moves the boundary from block 3 to block 2.
1057+
ctx = self._mctx()
1058+
bs = ctx.block_size_tokens
1059+
prompt = self._prompt(bs * 3 + 1)
1060+
ctx.add_request(self._req(ctx, prompt.clone()))
1061+
1062+
# Only the last block has Mamba state, so the clamped boundary has none
1063+
# and there is no earlier cached block to fall back to: skip nothing.
1064+
self._mamba_allocate_and_register(ctx, self._block_ids(ctx, 0, 3)[2:])
1065+
req = self._req(ctx, prompt.clone(), request_id=2)
1066+
matched, _, _, _, prefix_skip, eff_chunk = ctx._compute_prefix_match(req, len(prompt))
1067+
assert len(matched) == 3 and prefix_skip == 0 and eff_chunk == len(prompt)
1068+
1069+
# Same clamp, but the first block is also cached: back off to it rather
1070+
# than all the way to zero.
1071+
ctx2 = self._mctx()
1072+
p2 = self._prompt(bs * 3 + 1)
1073+
ctx2.add_request(self._req(ctx2, p2.clone()))
1074+
blocks2 = self._block_ids(ctx2, 0, 3)
1075+
self._mamba_allocate_and_register(ctx2, [blocks2[0], blocks2[2]])
1076+
req2 = self._req(ctx2, p2.clone(), request_id=2)
1077+
m2, _, _, _, ps2, ec2 = ctx2._compute_prefix_match(req2, len(p2))
1078+
assert len(m2) == 3 and ps2 == bs and ec2 == len(p2) - bs
1079+
10491080
@pytest.mark.internal
10501081
def test_batch_invariant_mamba_chunked_prefill_scheduler_alignment(self):
10511082
ctx = self._mctx(

0 commit comments

Comments
 (0)